Java基础丨泛型

前言

在 JDK1.5 中,提出了一种新的概念:泛型,那么什么是泛型呢?
它其实就是一种参数化的集合,限制了你添加进集合的类型。泛型的本质是为了参数化类型,也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型的意义

要看出泛型的好处,那就举例子说明没有泛型的坏处。

强制转换类型

public class TestGeneric {
    public static void main(String[] args) {
        // 1.没有泛型,可向集合中添加任意类型的对象,存在类型不安全风险。
        ArrayList arrayList = new ArrayList();
        arrayList.add("Hello");
        arrayList.add(2021);
        arrayList.add(false);
        for (int i = 0; i < arrayList.size(); i++) {
            // 强制类型转换会抛出 java.lang.ClassCastException 异常
            System.out.println((String)arrayList.get(i));
        }
    }
}

由于 ArrayList 内部就是一个 Object[]数组,所以添加字符串类型、数值类型、布尔类型都可以,但是在获取元素使用时都以 String类型使用,就会抛出错误。所以,在获取元素时要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常。

所以,泛型可以在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的。程序更加健壮:只要编译期没有警告,运行期就不会抛出 ClassCastException 异常;提高了代码的可读性:编写集合的时候,就限定了集合中能存放的类型。

代码复用

// 2.没有泛型,每次都要重复新增一个代码相似的
private static int add(int a, int b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}
// float
private static float add(float a, float b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}
// double
private static double add(double a, double b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

如果没有泛型的话,处理相同逻辑不同类型的需求会非常麻烦。如上所示,要实现不同类型的加法,每种类型都需要重载一个 add()方法;当然我们也可以声明传入参数为 Object,并在比较两个元素大小时,判断元素类型,并使用对应的方法比较。这样,代码就会恶心在类型判断上了,虽然不优雅的范围小了一点,并不能解决问题。
通过泛型,我们可以复用为一个方法:

// 使用泛型,提高代码复用
private static <T extends Number> double add(T a, T b) {
    System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
    return a.doubleValue() + b.doubleValue();
}

那么,可以总结一下泛型的应用场景了,当遇到以下场景时,我们可以考虑使用泛型:

  1. 当参数类型不明确,可能会扩展为多种时;
  2. 想声明参数类型为 Object,并在使用时用 instanceof 判断时。

需要注意,泛型只能替代 Object 的子类型,如果需要替代基本类型,可以使用包装类。

泛型的基本使用

泛型指代一种参数类型,可以声明在类、方法和接口上。

声明

泛型的声明使用 <占位符 [, 另一个占位符]> 的形式,需要在一个地方同时声明多个占位符时,使用逗号 , 隔开。占位符的格式并无限制,不过一般约定使用单个大写字母,如 T 代表类型(Type),E 代表元素(Element)等。
虽然没有严格规定,我们可以换成 A-Z 之间的任何一个字母都可以,并不会影响程序的正常运行。不过为了代码的易读性,最好使用前检查一下约定用法。
通常情况下,T、E、K、V、?是这样约定的:

  • ?表示不确定的 java 类型;
  • T(type) 表示具体的一个 java 类型;
  • K V(key value) 分别代表 java 键值中的 Key 与 Value;
  • E(element) 代表 Element。

泛型类

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

// JDK7 提供了一个可以略微减少代码量的泛型简写方式,后面可以只用<>
List<Integer> list = new ArrayList<>();
Set<Integer> set = new HashSet<>();
Map<String, Integer> map = new HashMap<>();

在实例化泛型类时,需要指明泛型类中的类型参数,并赋予泛型类属性相应类型的值。

一个最普通的自定义泛型类:

/**
 * 此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
 * 在实例化泛型类时,必须指定T的具体类型
 */
class Generic<T>{
    // key这个成员变量的类型为T,T的类型由外部指定
    private T key;
    public Generic(T key) {
        // 泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){
        // 泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

public class TestGeneric {
    public static void main(String[] args) {
        // 泛型的类型参数只能是对象类型(包括自定义类),不能是基本数据类型
        // 传入的实参类型需与泛型的类型参数类型相同,即为Integer.
        Generic<Integer> genericInteger = new Generic<Integer>(20210815);
        // 传入的实参类型需与泛型的类型参数类型相同,即为String.
        Generic<String> genericString = new Generic<String>("value");
        System.out.println(genericInteger.getKey());
        System.out.println(genericString.getKey());
    }
}

参照 HashMap<K,V> 类的定义,多元泛型的写法如下:

/**
 * 参照 HashMap<K,V>类的定义,此处指定了两个泛型类型
 */
class Notepad<K, V> {
    private K key;
    private V value;

    public K getKey() {
        return this.key;
    }

    public V getValue() {
        return this.value;
    }

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

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

public class TestGeneric {
    public static void main(String[] args) {
        // 定义两个泛型类型的对象
        Notepad<String, Integer> t = null ;
        // 里面的key为String,value为Integer
        t = new Notepad<String,Integer>() ;
        t.setKey("杰克") ;    
        t.setValue(18) ;
        System.out.print("姓名;" + t.getKey()) ;
        System.out.print(",年龄;" + t.getValue()) ;
    }
}

泛型接口

泛型接口与泛型类的定义及使用基本相同。

/**
 * 泛型接口
 */
interface Info<T> {
    // 定义抽象方法,抽象方法的返回值就是泛型类型
    public T getVar();
}
/** 实现类 */
class InfoImpl<T> implements Info<T> {
    // 定义属性
    private T var;

    public InfoImpl(T var) {
        // 通过构造方法设置属性内容
        this.setVar(var);
    }

    public void setVar(T var) {
        this.var = var;
    }

    @Override
    public T getVar() {
        return this.var;
    }
}

public class TestGeneric {
    public static void main(String[] args) {
        // 声明接口对象
        Info<String> i = null;
        // 通过子类实例化对象
        i = new InfoImpl<String>("杰斯");
        System.out.println("输出:" + i.getVar()) ;
    }
}

泛型方法

泛型同样可以在类中包含参数化的方法,而方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是不是泛型没有关系
泛型方法使得该方法能够独立于类而产生变化。如果使用泛型方法可以取代类泛型化,那么就应该只使用泛型方法。另外,对一个 static 的方法而言,无法访问泛型类的类型参数。因此,如果 static 方法需要使用泛型能力,就必须使其成为泛型方法。

定义泛型方法的语法格式如下:

[访问权限修饰符] [static] [final] <类型参数列表> 返回值类型 方法名([形式参数列表])

为什么要使用泛型方法呢?因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新 new 一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。

public class TestGeneric {
    // 定义泛型方法
    public static <T> void Show(T obj) {
        if (obj != null) {
            System.out.println(obj);
        }
    }

    public static void main(String[] args) {
        Show(Integer.valueOf("666"));
    }
}

参考

CSDN
Java全栈知识体系
打赏
评论区
头像
文章目录