Java泛型:类型安全与代码复用的完美结合

新星杯·14天创作挑战营·第14期 10w+人浏览 90人参与

目录

前言

一、概述

二、定义

三、使用

①Collection示例

②Map示例

四、自定义泛型

①泛型类

②泛型接口

③泛型方法

五、通配符

①无界通配符

②上界通配符

③下界通配符

④泛型中 extends 和 super 对比:

六、类型擦除

①无界泛型 ——> Object

②有界泛型 ——> 边界类型

③泛型方法(T ——> Object)

七、易踩坑的点

1、= 两边的泛型类型必须一致

2、使用通配符后不能添加数据

3、泛型数组创建的限制

4、泛型方法调用陷阱


前言

        在泛型出现之前,Java容器类(如 ArrayLIst )只能存储 Object 类型的对象

        这导致了两个主要问题:

        1、类型不安全:编译器无法检查类型是否正确

        2、需要强制类型转换:每次取出元素都需要手动转换类型

// 泛型出现前的代码示例
ArrayList list = new ArrayList();
list.add("Hello");
list.add(123);  // 可以添加任何类型

String str = (String) list.get(0);  // 需要强制转换
String str2 = (String) list.get(1); // 运行时异常:ClassCastException

        为了实现保持向后兼容的前提下引入类型安全机制,Java设计师们选择了类型擦除的方式来实现泛型

一、概述

泛型的概念是 JDK1.5 引入的

主要目的是为了解决 类型安全性 代码复用 的问题

它具有一个强大的特性:

允许我们在定义类、接口和方法时使用参数化类型

二、定义

// 定义一个简单的泛型类
public class Box<T> {
    private T content;
    
    public void setContent(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
}

// 使用示例
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello World");
String content = stringBox.getContent();  // 不需要强制转换

三、使用

大概了解之后,我们结合前面学习的集合来具体理解:

泛型中常见的类型参数命名约定:

K - Key(键)

V - Value(值)

E - Element(元素)

T - Type(类型)

N - Number(数字)

①Collection示例

Collection是一个泛型接口

泛型参数是E,add方法的参数类型也是E

//Collection接口定义,其是一个泛型接口
public interface Collection<E> {
    //省略...
    
    boolean add(E e);
}

案例:

public static void main(String[] args) {
    Collection<String> c = new ArrayList<String>();
    
    c.add("hello1");
    c.add("hello2");
    c.add("hello3");
    //编译报错,add(E e) 已经变为 add(String e)
    //int类型的数据1,是添加不到集合中去的
    //c.add(1);
    for(String str : c) {
        System.out.println(str);
   }
}

        可以看出,传入泛型参数后,add方法只能接收String类型的参数,其他类型的数据无法添加到集合中,同时在遍历集合的时候,也不需要我们做类型转换了,直接使用String类型变量接收就可以了,JVM会自动转换

        Collection<String> c = new ArrayList<String>();

        可简写为菱形泛型形式:

        Collection<String> c = new ArrayList<>();

②Map示例

//Map接口也是泛型接口
public interface Map<K,V> {
    //省略...
    
    V put(K key, V value);
    Set<Map.Entry<K, V>> entrySet();
}
public static void main(String[] args) {
    Map<Integer,String> map = new HashMap<>();
    
    //根据泛型类型的指定,put方法中的key只能是Integer类型,value只能是String类型
    map.put(1,"hello1");
    map.put(2,"hello2");
    map.put(3,"hello3");
    map.put(4,"hello4");
 
    //根据上面列出的源码可知,当前指定Map的泛型类型为:Map<Integer,String> map
    //entrySet方法返回的类型就应该是Set<Map.Entry<Integer, String>>
    Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
    for(Map.Entry entry:entrySet){
        System.out.println(entry.getKey()+" : "+entry.getValue());
   }
}

四、自定义泛型

Java中泛型分三种使用情况:

  • 泛型类
  • 泛型接口
  • 泛型方法

①泛型类

如果泛型参数定义在类上面

那么这个类就是一个泛型类

定义格式:

[修饰符] class 类名<泛型类型名1,泛型类型名2,...> { 
 0个或多个数据成员;
    0个或多个构造方法;
    0个或多个成员方法;
}
//注意:之前用确定数据类型的地方,现在使用自定义泛型类型名替代

示例(JDK中的 HashSet):

泛型类实例化对象格式:

泛型类名<具体类型1,具体类型2,...> 对象名 = new 泛型类名<>(实参列表);

案例展示:

        定义一个泛型类Circle

        包含 x,y 坐标和 radius 半径

基础泛型类:

//自定义泛型类:圆
//class 类名<泛型类型1,泛型类型2,...>
// 泛型类型名字可以自行定义
public class Circle<T, E> {
    //原来具体数据类型的地方,使用泛型类型名替换即可
    private T x;
    private T y;
    private E radius;
 
    //无参构造器没有任何改变
    public Circle() {}
    
    //原来具体数据类型的地方,使用泛型类型名替换即可
    public Circle(T x, T y, E radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }
 
    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 E getRadius() {
        return radius;
    }
    public void setRadius(E radius) {
        this.radius = radius;
    }
 
    @Override
    public String toString() {
        return "Circle [x=" + x + ", y=" + y + ", radius=" + radius + "]";
    }
}

测试类:

import com.briup.chap08.bean.Circle;
public class TestGenericsClass {
    public static void main(String[] args) {
        //实例化泛型类对象:
        // 泛型类<具体类型1,具体类型2,...> 对象 = new 泛型类<>(实参s);
 
        //1.实例化具体类对象,2种泛型设置为Integer和Double
        // 注意,泛型类可以是任意引用类型
        Circle<Integer, Double> c1 = new Circle<>(2,3,2.5);
        int x = c1.getX();
        double r = c1.getRadius();
        System.out.println("x: " + x + " radius: " + r);
 
        System.out.println("------------------");
        
        //2.实例化具体类对象,2种泛型设置为Double和Integer
        Circle<Double, Integer> c2 = new Circle<>(2.0,3.0,2);
        double x2 = c2.getX();
        int r2 = c2.getRadius();
 
        System.out.println("x2: " + x2 + " r2: " + r2);
    }
}
//运行结果:
x: 2 radius: 2.5
------------------
x2: 2.0 r2: 2

②泛型接口

如果泛型参数定义在接口上面,那么这个接口就是一个泛型接口

定义格式:

[修饰符] interface 接口名<泛型类型名1,泛型类型名2,...> { }

示例(JDK中的 Set 泛型接口):

// 定义泛型接口
public interface Container<T> {
    void add(T item);
    T get(int index);
    int size();
}

// 实现泛型接口
public class SimpleContainer<T> implements Container<T> {
    private List<T> items = new ArrayList<>();
    
    @Override
    public void add(T item) {
        items.add(item);
    }
    
    @Override
    public T get(int index) {
        return items.get(index);
    }
    
    @Override
    public int size() {
        return items.size();
    }
}

③泛型方法

如果泛型参数定义在方法上面,那么这个方法就是泛型方法

定义格式:

[修饰符] <泛型类型名> 返回值类型 方法名(形式参数列表){ 
    方法具体实现;
}

调用格式:

类对象.泛型方法(实参列表);
类名.static泛型方法(实参列表);

注意!
泛型方法调用时不需要额外指定泛型类型,系统会自动识别泛型类型

案例展示(接上面 Circle,补充泛型方法disp() 和 static show() ):

public class Circle<T,E> {
    //省略...
    
    //泛型类中定义 泛型方法
    public <F> void disp(F f) {
        System.out.println("in 泛型方法disp, f: " + f);
   }
    // 下面写法虽然不会报错,不建议大家这样写
    // 因为泛型方法上的T 会和 泛型类上的T 产生歧义
    public static <T> void show(T t) {
        System.out.println("in 泛型static方法show, t: " + t);
   }
}

测试类:

public static void main(String[] args) {
    Circle<Integer,Integer> c = new Circle<>();
    //public <F> void disp(F f);
    //调用时系统自动识别泛型方法类型
    c.disp(1); //Integer
    c.disp(2.3); //Double
    c.disp("hello");//String
    c.disp('h'); //Character
    System.out.println("--------------");
    //public static <T> void show(T t);
    //通过类名可以直接调用,不需要额外指定泛型类型
    Circle.show(2.3);
    Circle.show(2);
    Circle.show("hello");
}

//运行结果:
in 泛型方法disp, f: 1
in 泛型方法disp, f: 2.3
in 泛型方法disp, f: hello
in 泛型方法disp, f: h
--------------
in 泛型static方法show, t: 2.3
in 泛型static方法show, t: 2
in 泛型static方法show, t: hello

五、通配符

通配符可以匹配所有的泛型类型

主要分为三种:

  • 无界通配符        ?
  • 上界通配符        ?super T
  • 下界通配符        ?extends T

①无界通配符

观察以下代码:

public void test1(Collection<Integer> c) {
}
public void test2(Collection<String> c) {
}
public void test3(Collection<Double> c) {
}

可以发现他们只能接受一种类型的集合对象

原因是由于泛型的类型之间没有多态

所以 = 两边的泛型类型必须一致

这种情况就可以使用通配符 ?来表示泛型的父类型:

public void test(Collection<?> c) {

}
public static void main(String[] args){
    Test t = new Test();
    t.test(new ArrayList<String>());
    t.test(new ArrayList<Integer>());
    t.test(new ArrayList<Double>());
    t.test(new ArrayList<任意引用类型>());
}

②上界通配符

格式:

类型名<? extends 类型 > 对象名称

意义:

        只能接收该类型及其子类

public static void main(String[] args) {
    List<? extends Number> list;

    //list可以指向泛型是Number或者Number【子】类型的集合对象
    list = new ArrayList<Number>();
    list = new ArrayList<Integer>();
    list = new ArrayList<Double>();

    //编译报错,因为String不是Number类型,也不是Number的子类型
    //list = new ArrayList<String>();
}

能表示数字的类型都是Number类型的子类型,例如 Byte Short Integer Long 等

③下界通配符

格式:

类型名<? super 类型 > 对象名称

意义:

        只能接收该类型及其父类型

public static void main(String[] args) {
    List<? super Number> list;

    //list可以指向泛型是Number或者Number【父】类型的集合对象
    list = new ArrayList<Number>();
    list = new ArrayList<Serializable>();
    list = new ArrayList<Object>();

    //编译报错,因为String不是Number类型,也不是Number的父类型
    //list = new ArrayList<String>();
    
    //编译报错,因为Integer不是Number类型,也不是Number的父类型
    //list = new ArrayList<Integer>();
}

④泛型中 extends super 对比:

extends 可以定义泛型的 上限

这就表示将来泛型所接收的类型 最大 是什么类型

可以是这个 最大类型 或者它的 子类型 

super 可以定义泛型的 下限

这就表示将来泛型所接收的类型 最小 是什么类型

可以是这个 最小类型 或者它的 父类型 

六、类型擦除

泛型类型仅存在于编译期间

编译后的字节码和运行时不包含泛型信息

所有的泛型类型映射到同一份字节码

直接看代码就懂了:

// 编写时的代码
List<String> stringList = new ArrayList<String>();
List<Integer> intList = new ArrayList<Integer>();

// 编译后实际变成(简化理解)
List rawList1 = new ArrayList();  // 泛型信息被"擦除"
List rawList2 = new ArrayList();  // 泛型信息被"擦除"

为什么要这么做呢?

历史原因

        Java 5 引入泛型时,已经存在大量 没有泛型 的老代码和类库

        为了让新代码能和老代码兼容,Java 设计者采用了"类型擦除"方案。

注意!

泛型信息被擦除后,会转换为以下类型:

①无界泛型 ——> Object

(无界通配符同理)

// 擦除前
public class Box<T> {
    private T content;
    public T getContent() { return content; }
}

// 擦除后(概念上)
public class Box {
    private Object content;  // T 被替换为 Object
    public Object getContent() { return content; }
}

②有界泛型 ——> 边界类型

通配符同理,自动去掉 ?extends ?super

// 擦除前
public class NumberBox<T extends Number> {
    private T number;
    public T getNumber() { return number; }
    public double getValue() { return number.doubleValue(); } // 可以调用 Number 方法
}

// 擦除后(概念上)
public class NumberBox {
    private Number number;  // T 被替换为 Number
    public Number getNumber() { return number; }
    public double getValue() { return number.doubleValue(); }
}

多重边界的话会变成第一个边界

// 擦除前
public class ComparableSerializableBox<T extends Comparable<T> & Serializable> {
    private T value;
    public T getValue() { return value; }
}

// 擦除后(概念上)
public class ComparableSerializableBox {
    private Comparable value;  // 第一个边界类型
    public Comparable getValue() { return value; }
}

③泛型方法(T ——> Object)

// 擦除前
public static <T> T getFirst(List<T> list) {
    return list.get(0);
}

// 擦除后(概念上)
public static Object getFirst(List list) {  // T 被替换为 Object
    return list.get(0);
}

注意数组:

// 擦除前
public class GenericArray<T> {
    private T[] array;
    
    @SuppressWarnings("unchecked")
    public GenericArray(int size) {
        array = (T[]) new Object[size];  // 这里有陷阱!
    }
}

// 擦除后,实际创建的是 Object[] 数组

七、易踩坑的点

1、= 两边的泛型类型必须一致

//编译通过
//父类型的引用,指向子类对象
Object o = new Integer(1);

//编译通过
//Object[]类型兼容所有的【引用】类型数组
//arr可以指向任意 引用类型 数组对象
Object[] arr = new Integer[1];

//编译失败
//注意,这个编译报错,类型不兼容
//int[] 是基本类型数组
Object[] arr = new int[1];

//编译失败
//错误信息:ArrayList<Integer>无法转为ArrayList<Object>
//在编译期间,ArrayList<Integer>和ArrayList<Object>是俩个不同的类型,并且没有子父类型的关系
ArrayList<Object> list = new ArrayList<Integer>();

虽然 Integer Object 的子类型

但是 ArrayList<Integer> ArrayList<Object> 之间没有子父类型的关系

它们就是两个不同的类型

所以

Object o = new Integer(1); 编译通过

ArrayListlist<Objcet> = new ArrayList<Integer>();编译报错

也就是说

两个类型,如果是当做泛型的指定类型的时候,就没有多态的特点

2、使用通配符后不能添加数据

但是可以添加 null

Collection<?> c;     
c = new ArrayList<String>();

//编译报错
//因为变量c所声明的类型是Collection,同时泛型类型是通配符(?)
//那么编译器也不知道这个?将来会是什么类型,因为这个?只是一个通配符
//所以,编译器不允许使用变量c来向集合中添加新数据。
c.add("hello");

//编译通过
//有一个值是可以添加到集合中的,null
//集合中一定存的是引用类型,null是所有引用类型共同的一个值,所以一定可以添加进去。
c.add(null);

虽然不可以添加元素了,但是可以遍历集合取出数据:

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    list.add("hello1");
    list.add("hello2");
    list.add("hello3");
    list.add("hello4");
    Collection<?> c = list;
    
    //编译报错
    //c.add("hello5");
    for(Object obj : c) {
        System.out.println(obj);
   }
}

3、泛型数组创建的限制

为什么不能直接创建泛型数组?

// 这些都是非法的,编译器会报错
// T[] array = new T[10];                    // 错误
// List<String>[] lists = new List<String>[5]; // 错误
// List<T>[] genericArray = new List<T>[5];    // 错误

根本原因在于:

类型擦除导致运行时无法知道具体的类型参数

类型擦除与数组创建的冲突:

public class GenericArrayProblem<T> {
    // 假设这能编译通过
    // T[] array = new T[10];  // 如果 T = String,应该创建 String[]
    
    // 但是在运行时,JVM 看到的是:
    // Object[] array = new Object[10];  // 因为 T 被擦除为 Object
    
    // 这会导致类型安全问题
    public void demonstrateProblem() {
        // 如果允许这样做,会出现问题:
        // String[] stringArray = (String[]) new Object[10]; // ClassCastException
    }
}

那么如何正确创建泛型数组?

①使用 Object[] 并进行类型转换:

public class GenericArrayWithObject<T> {
    private Object[] array;
    private Class<T> type;
    
    public GenericArrayWithObject(Class<T> type, int size) {
        this.array = new Object[size];
        this.type = type;
    }
    
    @SuppressWarnings("unchecked")
    public void set(int index, T value) {
        // 运行时检查类型
        if (value != null && !type.isInstance(value)) {
            throw new ClassCastException("Expected " + type.getName() + 
                                       ", but got " + value.getClass().getName());
        }
        array[index] = value;
    }
    
    @SuppressWarnings("unchecked")
    public T get(int index) {
        return (T) array[index];  // 类型转换
    }
    
    public int size() {
        return array.length;
    }
}

②使用通配符创建数组:

public class WildcardArrayCreation {
    // 可以创建无界通配符数组,但有限制
    public void createWildcardArray() {
        // 直接创建仍然不行
        // List<?>[] array = new List<?>[10];  // 编译错误
        
        // 但可以通过类型转换创建
        List<?>[] array = (List<?>[]) new List[10];  // 未检查转换警告
        
        // 使用示例
        array[0] = new ArrayList<String>();
        array[1] = new ArrayList<Integer>();
        
        // 但不能添加元素(除了 null)
        // array[0].add("Hello");  // 编译错误
        array[0].add(null);  // 只能添加 null
    }
}

③通过反射创建泛型数组(这个后面遇到再说)

4、泛型方法调用陷阱

public class GenericMethodTrap {
    // 错误示例:泛型方法参数推断问题
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    public static void demonstrateTrap() {
        // 当传递 null 时,编译器无法推断类型
        // swap(null, 0, 1);  // 编译错误:无法推断类型
        
        // 解决方案1:显式指定类型参数
        swap((String[]) null, 0, 1);
        
        // 解决方案2:使用具体类型
        String[] strings = null;
        swap(strings, 0, 1);
    }
    
    // 错误示例:泛型集合不支持协变(协变:类型安全前提下,子类型替换父类型)
    public static void covarianceTrap() {
        // 这样做是不安全的
        // List<Object> objectList = new ArrayList<String>();  // 编译错误
        
        // 但是这样是可以的(使用通配符)
        List<? extends Object> objectList = new ArrayList<String>();
        
        // 注意:使用 extends 通配符后不能添加元素(除了 null)
        // objectList.add("Hello");  // 编译错误
        objectList.add(null);  // 只能添加 null
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值