JAVA中的泛型机制详解

 1.泛型的概念

java泛型是java5引入的一个特性,它允许我们为类,接口,方法指定类型参数,从而提供编译时类型安全检查。泛型的本质是参数化类型,即在声明类,接口或者方法时不指定具体的类型,而是使用一个或者多个类型参数表示

 泛型的好处主要有两个:

  • 类型安全:在编译的时候就能检查到类型的错误,而不是在运行的时候抛出ClassCastException
  • 消除强制类型转换:泛型可以自动进行类型的转换,从而减少代码中显性类型转换的需求,使得代码更加的简洁。

举个例子,泛型究竟是怎么诞生的:

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型测试","item = " + item);
}

运行结果:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

ArrayList可以存放任意类型,例子中添加了一个String类型,又添加了一个Integer类型,例子中我们从Arraylist中获取值用String类型去获取Integer不能自动类型转换,程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。

为了避免在运行时会出现这种情况,我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。

List<String> arrayList = new ArrayList<String>();

这样子,ArrayList里面只能存储String类型的值。

2.泛型方法

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 <E>)。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。

java 中泛型标记符:

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • ? - 表示不确定的 java 类型

泛型标记符在概念上都是相同的,它们都是用来指定泛型列,接口或者方法可以接受的类型。你可以根据自己的喜好或者代码的上下文来随意选择使用哪个标记符。

下面我们来做一个题目来练习一下: 

假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?

答案是可以使用 Java 泛型

使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。


    public class Main {
        //泛型方法PrintArray
        public static < E > void printArray(E[] inputArray){
            //输出数组元素
            for(E element:inputArray){
                System.out.printf("%s",element);
            }
            System.out.println();
        }
        public static void main(String[] args){
            Integer[] intArray ={1,2,3,4,5};
            Double[] doubles ={1.1,2.2,3.3,4.4};
            Character[] characters ={'H','E','L','L','O'};

            System.out.println("整数数组元素为、:");
            printArray(intArray);//传递一个整数数组

            System.out.println("\n双精度型数组元素为:");
            printArray(doubles);

            System.out.println("\n字符型数组元素为:");
            printArray(characters);
        }

    }

运行结果为:

整数数组元素为、:
12345

双精度型数组元素为:
1.12.23.34.4

字符型数组元素为:
HELLO

泛型的基本使用:

在类名之后使用尖括号声明类型参数,声明的类型参数可以像普通类型一样用在类型声明处使用,到使用时再决定其具体类型,然后编译器会帮我们处理一些类型类型转换的细节。

public class Holder<T> {
    T val;

    public Holder(T val) {
        this.val = val;
    }

    public T getVal() {
        return val;
    }
    
    public void setVal(T val) {
        this.val = val;
    }
    
    public static void main(String[] args) {
        Holder<String> strHolder = new Holder<String>("abc");
        String s = h.getVal();
    }
}

在使用时指定了的 Holder 的类型参数为 String。可以将 getVal() 的返回值直接赋给一个 String 变量,而不用显示的转型。在使用 setVal 时也必须传入 String 类或其子类,若入参不是 String 或其子类那么编译时会报错。

在Java7之前 new 参数化类型时需要指定类型,但在Java7之后 new 操作可以不用显示指定类型,编译器会自动推导出来:

 Holder<String> h = new Holder<>("abc");

2.1泛型方法的使用

泛型类,在创建类的对象的时候确定类型参数的具体类型;
泛型方法,在调用方法的时候再确定类型参数的具体类型。

泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用。
当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器就可以判断出类型参数 T所代表的具体数据类型。
 

举例如下:

public class Demo {  
  public static void main(String args[]) {  
    GenericMethod d = new GenericMethod(); // 创建 GenericMethod 对象  
    
    String str = d.fun("汤姆"); // 给GenericMethod中的泛型方法传递字符串  
    int i = d.fun(30);  // 给GenericMethod中的泛型方法传递数字,自动装箱  
    System.out.println(str); // 输出 汤姆
    System.out.println(i);  // 输出 30

	GenericMethod.show("Lin");// 输出: 静态泛型方法 Lin
  }  
}

class GenericMethod {
	// 普通的泛型方法
	public <T> T fun(T t) { // 可以接收任意类型的数据  
    	return t;
  	} 

	// 静态的泛型方法
	public static <E> void show(E one){     
         System.out.println("静态泛型方法 " + one);
    }
}  

3.认识泛型的写法

先从一个简单的泛型类开始:

public class Main {
    class point<T>{//此处可以随便写标识符号,T是type的简称
        private T var;//var的类型由T指定,即:有外部指定
        public T getVar(){
            return var;
        }
        public void setVar(T var){//设置的类型也由外部决定
            this.var=var;
        }
    }

    public void main(String[] args) {
        point<String> p =new point<String>();//里面的var类型为String类型
        p.setVar("it");
        System.out.println(p.getVar().length());
    }

}

多元泛型:

public class Main {
    static class Notepad<K,V>{
        private K key;
        private V value;

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }

    public static void main(String[] args) {
        Notepad<String,Integer> t =null;
        t=new Notepad<String,Integer>();
        t.setKey("汤姆");
        t.setValue(20);
        System.out.println("姓名"+t.getKey());
        System.out.println("年龄"+t.getValue());
    }

}

多个类型参数使用逗号分隔:

public class Holder<A, B, C> {

    public A v1;
    public B v2;
    public C v3;

    public Holder(A v1, B v2, C v3) {
        this.v1 = v1;
        this.v2 = v2;
        this.v3 = v3;
    }

    public static void main(String[] args) {
        Holder<String, Integer, Float> h = new Holder<>("abc", 1, 2.5);
    }
}

简单的泛型接口:

public class Main {
    interface Info<T>{        // 在接口上定义泛型
        public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型
    }
    static class InfoImpl<T> implements Info<T>{   // 定义泛型接口的子类
        private T var ;             // 定义属性
        public InfoImpl(T var){     // 通过构造方法设置属性内容
            this.setVar(var) ;
        }
        public void setVar(T var){
            this.var = var ;
        }
        public T getVar(){
            return this.var ;
        }
    }

        public static void main(String arsg[]){
            Info<String> i = null;        // 声明接口对象
            i = new InfoImpl<String>("汤姆") ;  // 通过子类实例化对象
            System.out.println("内容:" + i.getVar()) ;
        }
    }

非泛型的类(或者泛型类)中定义泛型方法

class Arraylist<E> {
    public <T> T[] toArray(T[] a) {
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    }
}

4.泛型通配符        

泛型通配符是JAVA泛型中的一个概念,它允许你在编写代码的时候对泛型类型参数的类型进行限制。

为什么要使用通配符?

  • 灵活性:通配符提供了一种灵活的方式来处理不同类型的集合,而不需要为每种类型编写特定的代码。
  • 类型安全:尽管通配符允许一定程度的类型不确定性,但它仍然提供了类型安全,防止了类型不匹配的错误。
  • 扩展性:使用通配符可以更容易的扩张代码。

这里的?代表类型是未知的,所以编译器不知道要检查哪种类型,因此不允许你向这样的列表中添加任何的元素。

Object是所有类的超类,因此任何类型的对象都可以安全的转换为Object类型。

无限制通配符:

使用?表示,它不指定任何类型的限制。这种通配符可以代表任何的类型,但使用它的时候,不能对它进行类型转换或者调用实例的方法

List<?> list =new ArrayList<>();

这个例子中,list可以存储任何类型的对象,但你不能调用list.get(0).toString()这样的操作,因为他的类型是未知的。

List<?> list = new ArrayList<String>();
list.add("Hello"); // 编译错误:不能添加具体类型
String s = list.get(0); // 编译错误:不能直接赋值给String
Object obj = list.get(0); // 正确:可以赋值给Object

使用无界通配符的集合可以被用来读取数据,但不能用来插入数据,因为插入需要知道具体的类型。

上界限定通配符:

使用?extend T 表示,它指定了参数类型的上限。意味着参数类型必须是T或者T的子类型。这种通配符常用于方法参数或者返回类型,以便能够处理更加广泛的类型。

   public void printList(List<? extends Number> list){
          for (Number num:list){
              System.out.println(num);

在这个例子中,printList方法可以接受任何的Number类型或其子类型的列表。

List<? extends Number> numberList = new ArrayList<Integer>();
numberList.add(5); // 编译错误:不能添加具体类型
Integer i = numberList.get(0); // 正确:可以赋值给Integer
Number num = numberList.get(0); // 正确:可以赋值给Number

在这个例子中,`numberList` 可以存储任何 `Number` 类型或其子类(如 `Integer`、`Double` 等)的对象。但是,你不能向这样的列表添加元素,因为添加操作需要知道具体的类型。

 下界限定通配符:

使用?super T表示,它指定了类型参数的下限,这意味着类型参数必须是T或者T的超类型。这种通配符常用于方法参数,特别是当你需要想集合中添加元素时。

public class Main {
      public void addToList(List<? super String>list, String item){
          list.add(item);
      }
    }

在这个例子中,addToList方法可以接受任何可以添加String类型元素的列表

List<? super Integer> list = new ArrayList<Number>();
list.add(5); // 编译错误:不能添加Number类型
list.add(new Integer(5)); // 正确:可以添加Integer类型
Number num = list.get(0); // 编译错误:不能赋值给Number
Integer i = list.get(0); // 正确:可以赋值给Integer

下界限定通配符使用 `? super T` 表示,它指定了类型参数的下限。这意味着类型参数必须是 `T` 或 `T` 的超类。

非泛型的类(或者泛型类)中定义泛型方法

class Arraylist<E> {
    public <T> T[] toArray(T[] a) {
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    }
}

5.类型擦除

JAVA中的类型擦除是一种语言特性,它允许编译器在编译的时候使用泛型类型,但是在运行的时候将泛型类型擦除,以保持向后兼容性。这意味着在运行的时候,泛型信息不再存在,所有的泛型类型都会被转换为它们的原始类型(即它们的边界类型,如果没有指定边界,则为object)

类型擦除的优点:

  • 兼容性:JAVA在1.5版本引入泛型,是为了保持与之前版本JAVA的代码兼容,JAVA设计者采用了类型擦除,这意味着在编译的时候,泛型的类型信息被擦除,只保留原始类型。
  • 性能:类型擦除避免了运行时的类型检查,这样可以减少运行时的开销,因为类型信息在编译的时候就已经被处理,运行时不需要额外的类型检查。
  • 安全性:虽然类型擦除可能会导致一些类型安全问题,但是JAVA通过一些机制来减少这些问题(比如泛型边界)。

1.无限制类型擦除

当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T><?>的类型参数都被替换为Object。

2.有限制类型擦除 

当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number><? extends Number>的类型参数被替换为Number<? super Number>被替换为Object。

3.擦除方法定义中的类型参数

擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。

如何证明类型擦除呢?

举个例子:


public class Main {
    static class Box<T>{
        private T t;

        public T getT() {
            return t;
        }

        public void setT(T t) {
            this.t = t;
        }
    }

    public static void main(String[] args) {
        Box<Integer> integerBox=new Box<>();
        integerBox.setT(123);
        Integer value =integerBox.getT();
        System.out.println("value: "+value);//输出结果为123
        
        //尽管Box<Integer>在编译的时候是Integer类型,但是在运行的时候它只是Box
        System.out.println(integerBox instanceof Box<Integer>);//编译的时候就报错了
        System.out.println(integerBox instanceof Box);//ture
        
    }
}

原始类型相等

public class Test {

    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass()); // true
    }
}

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。

有类型擦除和没有类型擦除的区别:

假设我们没有类型擦除,那么泛型信息将在运行时保留。然而,在Java中,由于类型擦除,运行时实际上并没有保留泛型的类型信息。以下是两种情况的对比:

没有类型擦除(假设情况):
Box<Integer> integerBox = new Box<>();
Box<String> stringBox = new Box<>();

// 假设我们可以通过反射获取泛型的实际类型参数
Type integerType = integerBox.getTypeParameter(); // 应返回Integer.class
Type stringType = stringBox.getTypeParameter(); // 应返回String.class
Box<Integer> integerBox = new Box<>();
Box<String> stringBox = new Box<>();

// 通过反射获取的类型参数将被擦除,无法获取Integer.class或String.class
Type integerType = integerBox.getClass().getTypeParameters()[0]; // 将返回null或Object.class
Type stringType = stringBox.getClass().getTypeParameters()[0]; // 同上

在Java中,由于类型擦除,我们无法在运行时获取泛型的类型参数。这意味着所有泛型类型参数在运行时都被替换为了它们的边界(如果没有指定边界,默认是Object),导致所有泛型类的实例在运行时看起来都是同一个类。

 泛型和类型擦除的影响:
•  类型安全:泛型在编译时提供了类型安全,防止了类型不匹配的错误。
•  运行时限制:由于类型擦除,我们不能在运行时区分不同泛型参数的类实例,也不能直接获取泛型的类型参数。
•  强制类型转换:在使用泛型对象时,可能需要进行强制类型转换,以确保类型安全。
泛型的类型擦除是Java设计时的一个权衡,它使得泛型能够在不引入新类型系统的情况下提供类型安全。然而,这也限制了泛型在运行时的应用,例如不能直接通过反射来操作泛型的类型参数。

类型擦除的应用场景:

类型擦除在Java中的一个典型应用场景是在集合框架中,尤其是List、Map等接口的实现。由于类型擦除,我们不能直接获取泛型参数的实际类型,这在编写通用的、类型安全的工具方法或容器类时尤其重要。
应用场景示例:编写一个类型安全的打印方法
假设我们想编写一个能够打印任何类型集合内容的方法,但由于类型擦除,我们无法直接知道集合中元素的确切类型。以下是一个简单的示例:

public class GenericsExample {
    // 一个通用的打印方法,但由于类型擦除,我们无法直接使用元素的确切类型
    public static void printList(List<?> list) {
        for (Object element : list) {
            System.out.println(element); // 只能以Object类型处理元素
        }
    }

    public static void main(String[] args) {
        // 创建不同类型的List
        List<Integer> intList = List.of(1, 2, 3, 4, 5);
        List<String> stringList = List.of("Hello", "World");

        // 打印List内容
        printList(intList); // 编译器知道元素类型是Integer,但运行时并不知道
        printList(stringList); // 同上,编译器知道元素类型是String

        // 尝试通过反射获取List的泛型类型
        System.out.println(intList.getClass().getTypeParameters()[0]); // 运行时输出null
        System.out.println(stringList.getClass().getTypeParameters()[0]); // 同上
    }
}

在这个示例中,printList方法是一个通用的打印方法,它接受一个List类型的参数。由于类型擦除,我们只能将列表中的元素当作Object来处理。尽管编译器在编译时知道List的具体类型参数(例如Integer或String),但在运行时这些信息是不可用的。

4.如何理解泛型的编译期检查?

既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?

Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。

public static  void main(String[] args) {  

    ArrayList<String> list = new ArrayList<String>();  
    list.add("123");  
    list.add(123);//编译错误  
}

在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

因为类型检查就是编译时完成的,new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正涉及类型检查的是它的引用,因为我们是使用它引用list1来调用它的方法,比如说调用add方法,所以list1引用能完成泛型类型的检查。而引用list2没有使用泛型,所以不行。

public class Test {  

    public static void main(String[] args) {  

        ArrayList<String> list1 = new ArrayList();  
        list1.add("1"); //编译通过  
        list1.add(1); //编译错误  
        String str1 = list1.get(0); //返回类型就是String  

        ArrayList list2 = new ArrayList<String>();  
        list2.add("1"); //编译通过  
        list2.add(1); //编译通过  
        Object object = list2.get(0); //返回类型就是Object  

        new ArrayList<String>().add("11"); //编译通过  
        new ArrayList<String>().add(22); //编译错误  

        String str2 = new ArrayList<String>().get(0); //返回类型就是String  
    }  
} 

通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象

泛型中参数话类型为什么不考虑继承关系

在Java中,像下面形式的引用传递是不允许的:

ArrayList<String> list1 = new ArrayList<Object>(); //编译错误  
ArrayList<Object> list2 = new ArrayList<String>(); //编译错误

实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。那么当我们使用list2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的),可是它里面实际上已经被我们存放了Object类型的对象,这样就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值