Java基础知识(八) -- 泛型

1.概述

1.1 什么是泛型?

  泛型,即“参数化类型”。提到参数,最熟悉的是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入实参与形参定义的数据类型不匹配,则会报错。

  那参数化类型是什么?以方法定义为例,在方法定义时,将方法中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量。泛型的本质是为了将类型参数化,可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。泛型的作用是可以在类声明时通过一个标识标识类中某个属性类型,或者是某个方法的返回值的类型,或者是参数类型。

1.2 应用场景

需求: 请编写程序,在 ArrayList 中,添加 3 个 Dog 对象, Dog 对象含有 name 和 age, 并输出 name 和 age (要求使用 getXxx())

import java.util.ArrayList;

@SuppressWarnings("{all}")
public class Generic02 {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        arrayList.add(new Dog("旺财", 10));
        arrayList.add(new Dog("发财", 1));
        arrayList.add(new Dog("小黄", 5));
        arrayList.add(new Cat("招财猫", 8));

        for (Object o : arrayList) {
            Dog dog = (Dog) o;
            System.out.println(dog.getName() + "-" + dog.getAge());
        }
    }
}

public class Dog {
    private String name;
    private int age;

    public Dog() {
    }

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Cat {
    private String name;
    private int age;

    public Cat() {
    }

    public Cat(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

上述代码在添加对象过程中, 不小心,添加了一只猫对象,程序运行的结果是什么?

在这里插入图片描述

上述代码在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常,其原因是 Cat 对象不能强转为 Dog 类型。那如何可以避免上述异常的出现?答案是 使用 泛型。代码如下:

@SuppressWarnings("{all}")
public class Generic02 {
    public static void main(String[] args) {
        ArrayList<Dog> arrayList = new ArrayList();
        arrayList.add(new Dog("旺财", 10));
        arrayList.add(new Dog("发财", 1));
        arrayList.add(new Dog("小黄", 5));

        for (Dog dog : arrayList) {
            System.out.println(dog.getName() + "-" + dog.getAge());
        }
    }
}

上述代码如果添加 Cat 对象时, 编译会报错。

2. 泛型的好处

  • ① 编译时, 检查添加元素的类型, 提高代码的安全性。
  • ② 减少了类型转换的次数,提高效率。在上述例子中不使用泛型的情况下: Dog-加入->Object-取出->Dog, 使用泛型的情况下: Dog->Dog->Dog
  • ③ 不在提示编译警告。

3.泛型类

3.1 泛型类的定义

  类型参数用于类的定义中,则该类被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map等。

3.2 泛型类的语法

  泛型的基本语法如下:

class 类名称 <泛型标识> {
  private 泛型标识 /*(成员变量类型)*/ 变量名; 
  .....

  }
}
  • 尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。
  • 泛型标识是任意设置的,Java 常见的泛型标识以及其代表含义如下:
    • T :代表一般的任何类。
    • E :代表 Element 元素,或者 Exception 异常。
    • K :代表 Key 。
    • V :代表 Value ,通常与 K 一起配合使用。
    • S :代表 Subtype 。
public class Generic<T> { 
    // key 的数据类型为 T(由外部指定)
    private T key;

	// 形参 key 的类型也为 T(由外部指定)
    public Generic(T key) { 
        this.key = key;
    }
    
	// 泛型方法 getKey 的返回值类型为 T(由外部指定)
    public T getKey(){ 
        return key;
    }
}

  在泛型类中,类型参数定义的位置有三处,分别为: ① 非静态的成员属性类型、② 非静态方法的形参类型(包括非静态成员方法和构造器)、③ 非静态的成员方法的返回值类型。

泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数

public class Test<T> {    
    public static T one;   // 编译错误    
    public static T show(T one){ // 编译错误    
        return null;    
    }    
}  

  泛型类中的类型参数的确定是在创建泛型类对象时,而静态变量和静态方法在类加载时已经初始化,直接使用类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的。

静态泛型方法中可以使用自身的方法签名中新定义的类型参数(即泛型方法),而不能使用泛型类中定义的类型参数。

public class Test2<T> {   
	// 泛型类定义的类型参数 T 不能在静态方法中使用  
    public static <E> E show(E one){ // 这是正确的,因为 E 是在静态方法签名中新定义的类型参数    
        return null;    
    }    
}  

泛型类不只接受一个类型参数,它还可以接受多个类型参数。

public class MultiType <E,T> {
	E value1;
	T value2;
	
	public E getValue1(){
		return value1;
	}
	
	public T getValue2(){
		return value2;
	}
}

3.3 泛型类的使用

  在创建泛型类的对象时,必须指定类型参数 T 的具体数据类型,即尖括号 <> 中传入的什么数据类型,T 便会被替换成对应的类型。如果 <> 中什么都不传入,则默认是 < Object >。

public class Point<T> {
    private T x;
    private T y;

    public Point() {
    }

    public Point(T x, T y) {
        this.x = x;
        this.y = y;
    }

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

public class TestPoint {
    public static void main(String[] args) {
        Point<Integer> p = new Point<>();
        p.setX(new Integer(100));
        System.out.println(p.getX());// 100


        Point<Float> p1 = new Point<>();
        p1.setX(new Float(100.12f));
        System.out.println(p1.getX());//100.12
    }
}

4.泛型接口

4.1 泛型接口的语法

public interface 接口名<T,R,...> {
    ...
}

注意事项:

  • ① 接口中,静态成员也不能使用泛型
  • ② 泛型接口的类型, 在继承接口或者实现接口时确定
  • ③ 没有指定类型,默认为 Object

4.2 泛型接口的使用

interface IUsb<U, R> {
    int n = 10;

    //普通方法中,可以使用接口泛型
    R get(U u);

    void hi(R r);

    void run(R r1, R r2, U u1, U u2);

    //在 jdk8 中,可以在接口中,使用默认方法, 也是可以使用泛型
    default R method(U u) {
        return null;
    }
}


//在继承接口 指定泛型接口的类型
interface IA extends IUsb<String, Double> {
}

//在实现 IUsu 接口的方法时,使用 String 替换 U, 是 Double 替换 R
class AA implements IA {
    @Override
    public Double get(String s) {
        return null;
    }

    @Override
    public void hi(Double aDouble) {
    }

    @Override
    public void run(Double r1, Double r2, String u1, String u2) {
    }
}

// 实现接口时,直接指定泛型接口的类型
class BB implements IUsb<Integer, Float> {
    @Override
    public Float get(Integer integer) {
        return null;
    }

    @Override
    public void hi(Float aFloat) {
    }

    @Override
    public void run(Float r1, Float r2, Integer u1, Integer u2) {
    }
}

//没有指定类型,默认为 Object
//建议直接写成 IUsb<Object,Object>
class CC implements IUsb { //等价 class CC implements IUsb<Object,Object> {
    @Override
    public Object get(Object o) {
        return null;
    }

    @Override
    public void hi(Object o) {
    }

    @Override
    public void run(Object r1, Object r2, Object u1, Object u2) {
    }
}

5.泛型方法

5.1 泛型方法的定义

  当在方法签名中的返回值前面声明了 < T > 时,该方法就被声明为泛型方法。< T >表明该方法声明了一个类型参数 T,并且类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数。

5.2 泛型方法的语法

基本语法:

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

注:

  • ① 只有在方法签名中声明< T >的方法才是泛型方法,仅使用泛型类定义的类型参数的方法并不是泛型方法。

    public class Test<U> {
    	// 该方法只是使用了泛型类定义的类型参数,不是泛型方法
    	public void testMethod(U u){
    		System.out.println(u);
    	}
    	
    	// <T> 真正声明了下面的方法是一个泛型方法
    	public <T> T testMethod1(T t){
    		return t;
    	}
    }
    
    
  • ② 泛型方法中可以同时声明多个类型参数。

    public class TestMethod<U> {
    	public <T, S> T testMethod(T t, S s) {
    		return null;
    	}
    }
    
  • ③ 泛型方法中也可以使用泛型类中定义的泛型参数。

    public class TestMethod<U> {
    	public <T> U testMethod(T t, U u) {
    		return u;
    	}
    }
    
  • ④ 泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的。

  • ⑤ 静态成员方法可以定义为泛型方法。

5.3 泛型方法的使用

  泛型类,在创建类的对象时确定类型参数的具体类型;泛型方法,在调用方法时再确定类型参数的具体类型。泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用。当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器就可以判断出类型参数 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);
    }
}  

6.类型擦除

6.1 泛型擦除的定义

  泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。

举例: 定义两个不同类型的ArrayList, 比较它们的类信息。

import java.util.ArrayList;

public class GenericType {
    public static void main(String[] args) {
        ArrayList<String> arrayString = new ArrayList<String>();
        ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
        System.out.println(arrayString.getClass() == arrayInteger.getClass());// true
    }
}

  在上述案例中,定义了两个 ArrayList 集合, 分别为 ArrayList< String> (存储字符串对象)和 ArrayList< Integer>(存储整型对象), 通过对象的 getClass() 方法获取它们的类信息并比较,发现结果为true。原因是在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Objec t>类型。

Q: 是不是所有的类型参数被擦除以后都以Object类进行替换呢?

答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数。

6.2 类型擦除的原理

① 不是说泛型信息在编译时就会被擦除掉吗?既然泛型信息被擦除了,如何保证在集合中只添加指定的数据类型的对象呢?

② 换而言之,定义了 ArrayList< Integer > 泛型集合,其泛型信息最终被擦除后就变成了 ArrayList< Object > 集合,那为什么不允许向其中插入 String 对象呢?

  在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。

public class GenericType {
    public static void main(String[] args) {  
        ArrayList<Integer> arrayInteger = new ArrayList<Integer>();  
        arrayInteger.add(111);
        Integer n = arrayInteger.get(0);// 此处会涉及类型转换的操作
        System.out.println(n);
    }  
}

7.泛型通配符

7.1 泛型通配符的定义

  在处理数据的过程中希望泛型能够处理某一类型范围内的类型参数,比如某个泛型类和它的子类,为此 Java 引入了泛型通配符的概念。泛型通配符有 3 种形式:

  • <?> :被称作无限定的通配符。
  • <? extends T> :被称作有上界的通配符。
  • <? super T> :被称作有下界的通配符。

7.2 上界通配符 <? extends T>

7.2.1 <? extends T> 的定义

  上界通配符 <? extends T>:T 代表了类型参数的上界,<? extends T>表示类型参数的范围是 T 和 T 的子类。需要注意的是: <? extends T> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。

public class GenericType {
    public static void main(String[] args) {  
		ArrayList<Number> list01 = new ArrayList<Integer>();// 编译错误

		ArrayList<? extends Number> list02 = new ArrayList<Integer>();// 编译正确
    }  
}

ArrayList< Integer > 和 ArrayList< Number > 之间不存在继承关系。而引入上界通配符的概念后,在逻辑上将 ArrayList<? extends Number> 看做是 ArrayList< Integer > 的父类,但实质上它们之间没有继承关系。

7.2.2 <? extends T> 的使用
import java.util.ArrayList;

public class Test {
    public static void main(String[] args) {
    	// 创建一个 ArrayList<Integer> 集合
        ArrayList<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);
        // 将 ArrayList<Integer> 传入 printIntVal() 方法
        printIntVal(integerList);
		
		// 创建一个 ArrayList<Float> 集合
        ArrayList<Float> floatList = new ArrayList<>();
        floatList.add((float) 1.0);
        floatList.add((float) 2.0);
        // 将 ArrayList<Float> 传入 printIntVal() 方法
        printIntVal(floatList);
    }
    
    public static void printIntVal(ArrayList<? extends Number> list) {
 		// 遍历传入的集合,并输出集合中的元素       
        for (Number number : list) {
            System.out.print(number.intValue() + " ");
        }
        System.out.println();
    }
}
// 输出结果:
// 1 2
// 1 2

分析:

  • 在 printIntVal() 方法中,其形参为 ArrayList<? extends Number>,因此,可以给该方法传入 ArrayList< Integer >、ArrayList< Float > 等集合。
  • 在 printIntVal() 方法内部,必须要将传入集合中的元素赋值给Number 对象,而不能赋值给某个子类对象; 是因为根据 ArrayList<? extends Number> 的特性,并不能确定传入集合的数据类型(即不能确定传入的是 ArrayList< Integer > 还是 ArrayList< Float >)。
7.2.3 <? extends T> 的小结

使用 extends 通配符表示可以读,不能写。

7.3 下界通配符 <? super T>

7.3.1 <? super T> 的定义

  下界通配符 <? super T>:T 代表了类型参数的下界,<? super T>表示类型参数的范围是 T 和 T 的超类,直至 Object。需要注意的是: <? super T> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。

public class GenericType {
    public static void main(String[] args) {  
		ArrayList<Integer> list01 = new ArrayList<Number>();// 编译错误

		ArrayList<? super Integer> list02 = new ArrayList<Number>();// 编译正确
    }  
}

ArrayList<? super Integer> 在逻辑上表示为 Integer 类以及 Integer 类的所有父类,它可以代表 ArrayList< Integer>、ArrayList< Number >、 ArrayList< Object >中的某一个集合,但实质上它们之间没有继承关系。

7.3.2 <? super T> 的使用
public class Test {
	public static void main(String[] args) {
		// 创建一个 ArrayList<? super Number> 集合
		ArrayList<Number> list = new ArrayList(); 
		// 往集合中添加 Number 类及其子类对象
		list.add(new Integer(1));
		list.add(new Float(1.1));
		// 调用 fillNumList() 方法,传入 ArrayList<Number> 集合
		fillNumList(list);
		System.out.println(list);
	}
	
	public static void fillNumList(ArrayList<? super Number> list) {
		list.add(new Integer(0));
		list.add(new Float(1.0));
	}
}
// 输出结果为:[1, 1.1, 0, 1.0]

  与带有上界通配符的集合ArrayList<? extends T>的用法不同,带有下界通配符的集合ArrayList<? super Number> 中可以添加 Number 类及其子类的对象;ArrayList<? super Number>的下界就是ArrayList<Number>集合,因此,其中必然可以添加 Number 类及其子类的对象;但不能添加 Number 类的父类对象(不包括 Number 类)。

7.3.3 <? super T> 的小结

使用 super 通配符表示可以写,不能读。

7.4 无限定通配符<?>

  无界通配符<?>:? 代表了任何一种数据类型,能代表任何一种数据类型的只有 null。值得注意的是:<?> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。Object 本身也算是一种数据类型,但却不能代表任何一种数据类型,所以 ArrayList< Object > 和 ArrayList<?> 的含义是不同的,前者类型是 Object,也就是继承树的最高父类,而后者的类型完全是未知的;ArrayList<?> 是 ArrayList< Object > 逻辑上的父类。

  • ① ArrayList<?> 在逻辑上表示为所有数据类型的父类,它可以代表 ArrayList< Integer>、ArrayList< Number >、ArrayList< Object >中的某一个集合,但实质上它们之间没有继承关系。
  • ② ArrayList<?> 既没有上界也没有下界,因此,它可以代表所有数据类型的某一个集合,但不能指定 ArrayList<?> 的数据类型。
  • ③ 大多数情况下,可以用类型参数 < T > 代替 <?> 通配符。

7.5 <? extends T>与<? super T> 对比

类别区别
<? extends T>编译器将只允许读操作,不允许写操作。即只可以取值,不可以设值。
<? super T>编译器将只允许写操作,不允许读操作。即只可以设值(set 操作),不可以取值(get 操作)。

  作为方法形参,<? extends T> 类型和 <? super T> 类型的区别在于:

  • <? extends T> 允许调用读方法T get()获取 T 的引用,但不允许调用写方法 set(T)传入 T 的引用(传入 null 除外)。
  • <? super T> 允许调用写方法set(T)传入 T 的引用,但不允许调用读方法 T get()获取 T 的引用(获取 Object 除外)。
public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }
}
  • copy() 方法的作用是把一个 List 中的每个元素依次添加到另一个 List 中。它的第一个形参是 List<? super T>,表示目标 List,第二个形参是 List<? extends T>,表示源 List。
  • copy() 方法的另一个好处是可以安全地把一个 List< Integer >添加到 List< Number >,但是无法反过来添加。

8.总结

在这里插入图片描述
本文仅作为个人学习记录所用, 不做任何商业所用。

参考链接:https://blog.youkuaiyun.com/weixin_45395059?type=blog

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值