一文读懂泛型

什么是泛型

泛型:在定义类,接口,方法时,可以把类型当作参数。

为什么需要泛型

泛型是在JDK1.5中添加的新特性。
在JDK1.5之前,为了让代码可以应用于多种类型,需要将参数设置为Object类。
例如我们在创建一个ArrayList时,内部实际是用一个Object的数组实现的。(为了向上兼容,现在创建ArrayList时,不指定类型的话,还是会被当作Object类)。
这样做的缺点是:任意类型的数据都可以存到这个ArrayList,在取出数据时,我们可能需要强制转型成目标类型,这样很容在运行期间出现转型错误。

List list = new ArrayList();
list.add("abc");
list.add(1);

for (Object o : list) {
    String s = (String) o;
    System.out.println(s.charAt(0));
}

当然这种错误也可以通过在转型之前进行类型检查来避免。

List list = new ArrayList();
list.add("abc");
list.add(1);

for (Object o : list) {
    if (o instanceof String) {
        String s = (String) o;
        System.out.println(s.charAt(0));
    }
}

但是每次都要这么搞,确实很麻烦.

因此在JDK1.5中引入了泛型,泛型最主要的作用就是通过指定参数类型,使编译器可以在编译期间检查程序中是否有类型转换的错误。

三种泛型的实现

在定义类时,把类型当作参数

格式如下,可以使用多个类型参数。

class 类名<类型参数1, 类型参数2...> {

}

代码实现如下:

class Map<K,V> {

    private K key;
    private V value;

    public Map(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

}

使用方法如下;在创建对象时声名类型参数代表的类型

public class Test_2 {

    public static void main(String[] args) {
        Map<Integer, String> map1 = new Map<>(1, "a");
        System.out.println(map1.getKey() + ": " + map1.getValue());
        Map<String, Integer> map2 = new Map<>("a", 1);
        System.out.println(map2.getKey() + ": " + map2.getValue());
    }

}

需要注意的时map1不能引用map2,因为他们的类型参数不同,如果让map1引用map2编译器会报错

错误: 不兼容的类型: Map<Integer,String>无法转换为Map<String,Integer>
        map2 = map1;
在定义方法时,把类型当作参数

每一个方法可以有它们自己的类型参数,并且是独立于所属类的类型参数的

格式如下:

<类型参数1, 类型参数2...> 返回值 方法名(参数...)

代码实现如下:

public static <K,V> boolean compare(Map<K, V> map1, Map<K, V> map2) {
    return Objects.deepEquals(map1, map2);
}

使用方法如下,编译器可以自动检测方法的参数的类型,所以不用指定方法的类型参数:

public static void main(String[] args) {
    Map<Integer, String> m1 = new HashMap<>();
    m1.put(1, "a");
    Map<Integer, String> m2 = new HashMap<>();
    m2.put(1, "a");
    System.out.println(compare(m1, m2));
}
在定义接口时,把类型当作参数

使用格式:

interface 接口名<类型参数1, 类型参数2...> {

}

例如:

interface GenericsInterface<K, V> {

    void printKV(K k, V v);

}

我们有两种方式来实现这个接口

  1. 在定义实现类时不明确类型参数,在创建对象时才明确类型参数
  2. 在定义实现类时明确类型参数
在定义实现类时不明确类型参数,在创建对象时才明确类型参数
class GenericsImpl1<K, V> implements GenericsInterface<K, V> {

    @Override
    public void printKV(K k, V v) {
        System.out.println(k);
        System.out.println(v);
    }

}
在定义实现类时明确类型参数
class GenericsImpl2 implements GenericsInterface<String, Integer> {

    @Override
    public void printKV(String s, Integer integer) {
        System.out.println(s.toUpperCase());
        System.out.println(integer);
    }

}

测试代码如下

public static void main(String[] args) {
    GenericsInterface<Integer, String> g1 = new GenericsImpl1<>();
    g1.printKV(1, "a");
    GenericsInterface<String, Integer> g2 = new GenericsImpl2();
    g2.printKV("a", 1);
}

受限的类型参数

有些时候可能只希望只能处理特定类型,例如只希望能处理List类型,那么就可用对类型参数进行限制。格式如下:

<类型参数1 extends 限制类型, 类型参数2 extends 限制类型...>

使用方法如下:

class Box<E extends List> {

    private E e;

    public E getE() {
        return e;
    }

    public void setE(E e) {
        this.e = e;
    }
    
}

测试代码如下

public static void main(String[] args) {
    Box<ArrayList> b1 = new Box<>();
    //        Box<HashMap> b2 = new Box<HashMap>(); 编译器会报错
}

使用泛型之后的继承关系

diagram showing that Box is not a subtype of Box

通配符

例如List, List会被视为两种不同的类型。如果代码中需要同时接受这两种类型,就可以用通配符来实现

通配符用<?>表示,代表不确定的类型,可以在定义参数,属性和本地变量的类型时使用(在定义返回值类型时也可以用,但是不建议用)

没有限制的通配符

没有限制的通配符就是<?>, 如List<?>,需要注意的是,使用通配符代表类型的对象会被视为Object类,只能调用Object类提供的方法。

代码实现如下。

public static void printList(List<?> list) {
    for (Object o: list) {
        System.out.println(o);
    }
}

测试代码如下:

public static void main(String[] args) {
    List<Integer> list1 = new ArrayList<>();
    list1.add(1);
    list1.add(2);
    printList(list1);
    List<String> list2 = new ArrayList<>();
    list2.add("abc");
    list2.add("cba");
    printList(list2);
}

运行结果

1
2
abc
cba

Process finished with exit code 0
有上限的通配符

与受限的类型参数的类似,使用格式为:<? extends 类名>,被修饰的参数必须是该类的子类,使用通配符代表类型的对象的类型会被视为该类。

代码实现如下。

public static void printNumberList(List<? extends Number> list) {
    for (Number n: list) {
        System.out.println(n);
    }
}

测试代码如下:

public static void main(String[] args) {
    List<Integer> list1 = new ArrayList<>();
    list1.add(1);
    list1.add(2);
    printNumberList(list1);

    List<Long> list2 = new ArrayList<>();
    list2.add(1l);
    list2.add(2l);
    printList(list2);
    List<String> list3 = new ArrayList<>();
    //        printNumberList(list2); 编译器会报错
}

运行结果如下:

1
2
1
2

Process finished with exit code 0
有下限的通配符

与受限的类型参数的类似。不同的是使用格式为:<? super 类名>,被修饰的参数必须是该类的父类,使用通配符代表类型的对象的类型会被视为Object 类。

public static void printIntegerList(List<? super Integer> list) {
    for (Object o: list) {
        System.out.println(o);
    }
}

测试代码如下

public static void main(String[] args) {
    List<Integer> list1 = new ArrayList<>();
    list1.add(1);
    list1.add(2);
    printIntegerList(list1);

    List<Number> list2 = new ArrayList<>();
    list2.add(1);
    list2.add(2);
    printIntegerList(list2);

    List<Long> list3 = new ArrayList<>();
    //        printIntegerList(list3);
}

运行结果如下

1
2
1
2

Process finished with exit code 0
带有通配符的继承关系

img

类型擦除

Java中的泛型是通过类型擦除实现的。

类型擦除就是把泛型对象当作其泛型类型参数的边界类型或者Object类处理。

例如, <?>会被视为Object类型;<T extends 类1>,<? extends 类1>就会被视为类1类型;<? super 类2>就会被是为类2的类型。

类型擦除的缺点

首先我们来看下面这两个类

class Box<E> {

    E data;

    public void setData(E data) {
        this.data = data;
    }

}

class MyBox extends Box<Integer> {

    public void setData(Integer data) {
        System.out.println("MyBox setData");
        super.setData(data);
    }

}

经过类型擦除之后,这两个类的会变成如下代码

class Box<Object> {

    Object data;

    public void setData(Object data) {
        this.data = data;
    }

}

class MyBox extends Box<Integer> {

    public void setData(Integer data) {
        System.out.println("MyBox setData");
        super.setData(data);
    }

}

来看下下面这段测试代码,因为我们没有在MyBox类中重写public void setData(Object data)方法,因为多态的存在,box.setData("data")调用的的Box类中的方法。所以可以推断出可以运行到Integer data = myBox.data这句代码并且会有ClassCastException(因为Bridge Methods,实际上不会运行到这句代码)

public static void main(String[] args) {
    MyBox myBox = new MyBox();
    Box box = myBox;
    box.setData("data");
    Integer data = myBox.data; // 这里会报错java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
}

为了解决这个问题,编译器在编译期间会生成一个Bridge Methods

Bridge Methods

生成的Bridge Methods如下,不过在java文件中看不到,字节码文件中才能看到。

class MyBox extends Box<Integer> {

    public void setData(Object data) {  // Bridge Methods
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyBox setData");
        super.setData(data);
    }

}

因为这个Bridge Methods的存在,在编译期间会有警告信息。(只要消除编译期间的所有warings, errors就不会出现类型转换异常)

注: test8\Test_8.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。

加上-Xlint:unchecked再编译下

test8\Test_8.java:10: 警告: [unchecked] 对作为原始类型Box的成员的setData(E)的调用未经过检查
        box.setData("data");
                   ^
  其中, E是类型变量:
    E扩展已在类 Box中声明的Object
1 个警告

当然,这只是警告,我们还是可以强行运行这个程序,不过运行期间会报错。

Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
	at test8.MyBox.setData(MyBox.java:3)
	at test8.Test_8.main(Test_8.java:10)

Process finished with exit code 1

可以看到错误发生在Test_8.java:10,也就是box.setData("data");这行代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,关于Java泛型类型的参数传入,我们需要先了解一下Java泛型的基本概念。 Java中的泛型是一种参数化类型的概念,即在定义类、接口或方法时,使用一个或多个类型参数来表示其中的某些类型,这些类型参数在使用时再被具体化。通过使用泛型,可以使代码更加通用、安全和可读性更强。 Java中的泛型类型参数可以用于类、接口和方法的定义中。在使用时,需要将具体的类型参数传递给它们,以指定其中的泛型类型。 下面以一个简单的例子来说明Java中参数传入泛型类型的用法。 ``` public class Box<T> { private T data; public Box(T data) { this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } } ``` 在这个例子中,我们定义了一个泛型类Box,其中的类型参数T可以在类的定义中被指定。在Box类的构造函数和getData、setData方法中,我们使用了泛型类型T来表示其中的某些类型。 现在我们可以创建一个Box对象,并将一个具体的类型参数传递给它,以指定其中的泛型类型。例如: ``` Box<Integer> box = new Box<Integer>(new Integer(10)); ``` 在这个例子中,我们创建了一个Box对象,并将Integer类型作为泛型类型参数传递给它。这样一来,我们就可以在Box对象中存储和获取Integer类型的数据了。 同样地,我们也可以创建其他类型的Box对象,例如: ``` Box<String> box = new Box<String>("Hello World!"); Box<Double> box = new Box<Double>(new Double(3.14)); ``` 通过这种方式,我们可以方便地定义、使用和重用泛型类型,从而使代码更加通用和灵活。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值