java基础——泛型

1. 什么是泛型?

Java泛型(Generics)是JDK5中引入的一种参数化类型特性。在定义类、接口或方法时使用类型参数,以便在使用时指定具体的数据类型。

参数化类型: 把类型当参数一样传递。

  • Person<T>:"T"为类型参数,整体为泛型类型。

  • Person<Student>:"Student"为实际类型参数,整体为参数化类型ParameterizedType。

2. 为什么要使用泛型?

为什么要使用泛型,泛型的优点是什么?

2.1 代码更灵活,可以复用

首先一点,对于同样一段代码,由于传入参数不同,必须重写方法。也就是需要传入多种数据类型,执行相同逻辑的代码。

public int add(int a, int b){
    return a + b;
}
public float add(float a, float b){
    return a + b;
}
public long add(long a, long b){
    return a + b;
}
...

对于不同数据类型的加法需要写不同方法去实现,这时可以使用泛型:

public static <T extends Number> T add(T a, T b) {
    if (a instanceof Integer) {
        return (T) Integer.valueOf(a.intValue() + b.intValue());
    } else if (a instanceof Double) {
        return (T) Double.valueOf(a.doubleValue() + b.doubleValue());
    } else if (a instanceof Float) {
        return (T) Float.valueOf(a.floatValue() + b.floatValue());
    } else if (a instanceof Long) {
        return (T) Long.valueOf(a.longValue() + b.longValue());
    } else if (a instanceof Short) {
        return (T) Short.valueOf((short) (a.shortValue() + b.shortValue()));//在Java中,当你进行整数运算时,如果操作数的类型是 short 或 byte,结果会自动提升为 int。
    } else if (a instanceof Byte) {
        return (T) Byte.valueOf((byte)(a.byteValue() + b.byteValue()));
    } else {
        throw new IllegalArgumentException("不支持的数据类型");
    }
}

2.2 消除强转,使代码更简洁

未使用泛型时,需要进行强转:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

使用泛型时,不需要类型强转:

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);

2.3 将类型转换错误提前到编译期

使用泛型,编译时进行更强大的类型检查,若编译期没有警告,运行期就不会出现ClassCastException。

List list = new ArrayList();
list.add("hello");
list.add("你好");
list.add(1);
for(int i = 0; i < list.size(); i++){
    String s = (String) list.get(i);
    sout(s);
}

上述代码没有使用泛型,在编译期不会报错,但是运行期会出现ClassCastException

在声明集合时指定了类型,如果尝试添加其他类型的元素,编译器会报错,将类型转换错误提前到编译期,这样会使代码更加健壮。

3. 泛型使用的三种情况

3.1 泛型类

创建一个泛型类型声明,引入了类型变量T,在类中的任何位置都可以使用:

public class Box<T> {
    private T t;
    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

要从代码中引用泛型Box类,必须执行泛型类型调用,如:Box<Integer> integerBox;

注:

1. 泛型类型变量不能使用基本数据类型。(类型擦除下文会介绍)

ArrayList<int> arrays = new ArrayList<>();会报错。
擦除后变成Object,而Object没法存int。

2.不能使用instanceof运算符。

擦除后只剩下原始数据类型,泛型信息不存在了。

常用的类型形参名称:

  • E - Element(Java 集合框架广泛使用)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

3.2 泛型接口

定义方法同泛型类。

3.3 泛型方法

泛型方法中的类型形参的范围仅限于声明它的方法,允许使用静态非静态方法,以及泛型类构造函数。

类型形参部分必须出现在方法的返回类之前。

public class Util {
    //静态方法
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {//可定义多个类型参数
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair<K, V> {

    private K key;
    private V value;

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

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

调用此方法的完整语法如下:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util. <Integer, String> compare(p1, p2);

通常,已明确提供该类型时,可以省略,允许你将泛型方法作为普通方法调用,而无需在尖括号之间指定类型,编译器将推断所需的类型,此功能称为 type inference (类型推断)

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);

4. 通配符 & 类型变量的限定

通配符分类:

通配符
限定通配符
上界通配符
下界通配符
非限定通配符

通配符:?,表示未知类型。

Q:通配符与泛型有什么关系?

A:使用通配符的目的就是为了灵活转型,通配符让泛型转型更灵活,限制类型实参的类型,使用有界类型形参。

public <T extends Person> void getPerson(T t){
    sout(t.getClass().getName());
}

4.1 is-a关系

在具体介绍通配符之前,首先了解一下is-a关系:

只要类型兼容,就可以将一种类型的对象分配给另一种类型的对象:

Object o = new Object();
Integer i = new Integer(1);
o = i;

在面向对象中这是一种“ 是一个(is a)”关系。由于Integer is a(是一个)Object,因此允许赋值。

同理,泛型也是如此:

Box<Number> box = new Box<Number>();
box.add(new Integer(1));
box.add(new Double(1.1));

4.2 上界通配符

上界通配符:<? extends Person>,extends后跟着上界,该类型是继承(或实现)Person类(或接口)的。

上界通配符的副作用
使用上界通配符可以取元素,但不可以存放元素。

Room<? extends Person> studentRoom = teacherRoom;
studentRoom.set(new Sudent());//报错,无法放任何元素,也不可以取
studentRoom.set(new Teacher());//报错,无法放任何元素
studentRoom.set(null);//true,可以放null
studentRoom.get();//也是不可以的
Person person = studentRoom.get();//true
Object o = student.get();//true

Q:为什么使用上界通配符,不能存放元素了?

A: 字节码中是使用标记指定泛型类型,<? extends Person>这个类型也是使用标记表示,事实上并不知道他是什么类型,他可以是Person或Person子类型中任何一个,既无法与Sudent的标记对应,也无法与Teacher的标记对应,所以不能放任何元素。

注:通过反射,什么元素都能放,该方法自己用用就行。

4.3 下界通配符

下界通配符:<? super Person>,super后跟着下界,Person类是该类型的基类。

使用下界通配符,可以存放元素,但不可以取元素:

Room<? super Person> studentRoom = new Room<Creature>;
studentRoom.set(new Sudent());//true
studentRoom.set(new Teacher());//true
studentRoom.set(new Creature());//不可以
Person person = studentRoom.get();//不可以,限定的是下界
Object o = student.get();//true

4.4 非限定通配符

Room<?>: 非限定通配符,是一个泛型类型,等价于Room<? extends Object>

注:非限定通配符既不能读也不能写。

Q:List<?>与List有什么区别?

A:List不进行类型安全检查,List<?>进行类型安全检查。

4.5 多重边界

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }

如果边界中含有类,必须第一个指定,如上述代码中A,否则会编译出错。

注:有界形参除了限制类型外,还允许你调用边界中定义的方法。

4.6 Java泛型的PECS原则

PECS:Producer extends Consumer super。

以集合为例:

  • 如果只需要从集合中获得类型T,使用 <? extends T> 通配符。
  • 如果只需要将类型T放入集合中,使用 <? super T> 通配符。
  • 如果既要获取又要放置元素,则不使用任何通配符。

5. 在Java中如何处理泛型&类型擦除

5.1 Java泛型的原理

private static void test1() {
    ArrayList<Apple> apples = new ArrayList<>();
    ArrayList<Banana> bananas = new ArrayList<>();
    System.out.println(apples.getClass() == bananas.getClass());//true
}

上述代码中打印信息居然为true,为什么呢?

以ArrayList中的set方法为例,查看生成的字节码文件,如下图所示:

image.png

注:使用 javac 命令编译源文件,使用 javap -c 命令查看生成的字节码。

从中可以发现,泛型E在字节码文件中被擦除为Object,为什么会这样呢?

Q: Java泛型的原理是什么?什么是泛型擦除机制? 泛型是JDK5后引入的,兼容性怎么样,使用是否有影响?

A: Java的泛型是JDK5引入的新特性,虚拟机是不支持泛型的,为了向下兼容,Java实现的其实是一种伪泛型机制,只是在编译器进行处理的时候去做类型转换等操作,但是字节码还是之前的字节码,Java在在编译期擦出了所有的泛型信息,这样Java就不需要产生新的类型到字节码,在Java运行时根本不存在泛型信息。

接着看如下代码:

public class Banana<T extends Banana> extends Fruit<T>{
    @Override
    public void set(T t) {
        super.set(t);
    }
}

查看字节码文件

image.png

Q:字节码中为什么会有两个set方法呢?

A: 为了保证继承的多态性,自动生成了一个桥方法。在调用第二个set方法时,先判断是否是Banana,然后强转。

5.2 Java编译器具体是如何擦除泛型的?

1. 检查泛型类型,获取目标类型。

2. 擦除类型变量,并替换为限定类型.

  • 如果泛型类型的类型变量没有限定(<T>),则用 Object 作为原始类型。
  • 如果有限定(<T extends Person>),则用 Person 作为原始类型。
  • 如果多个限定,则使用第一个边界作为原始类。

3. 在必要时插入类型转换以保证类型安全。

4. 生成桥方法以在扩展时保持多态性。

5.3 泛型擦除残留

查看Banana类的class文件:

image.png

class文件中泛型仍为T,这是泛型擦除的残留。这里看到的其实是签名而已,保留了定义的格式,对分析字节码有好处的。这个信息存在类的常量池中。

泛型类中独有的标记,普通类没有,JDK5才加入,标记了定义时的成员签名。

5.4 泛型与反射

Q:泛型被擦除了,为什么还与反射有关?
A:擦除后,在类的常量池中保留了泛型信息,还能拿到它的信息。

public static void main(String[] args) throws NoSuchFieldException {
    test2();
}
ArrayList<String> arrayList;
private static void test2() throws NoSuchFieldException {
    Field field = MyClass.class.getDeclaredField("arrayList");
    System.out.println(field.getGenericType());//java.util.ArrayList<java.lang.String>
    ParameterizedType type = (ParameterizedType) field.getGenericType();
    System.out.println(type);//java.util.ArrayList<java.lang.String>
}

6. 有关泛型的几个小问题

6.1 泛型在静态方法和静态类中的问题

image.png

Q:为什么上述图片中泛型会报错?

A: 静态域或方法中不能引用类型变量。泛型参数的实例化需要在定义泛型类型对象时指定。而静态成员是不需要使用对象来调用的,所以不需要创建对象,自然就无法确定泛型参数是什么。

这里注意,泛型方法可以,因为这个T并不是Test<T>中的T,以下代码是正确的:

public static <T> T test(T t){
    return t;
}

6.2 泛型类型中的方法冲突

image.png

Q:为什么会报错

A: 擦除后两个方法一样了。

6.3 无法创建泛型实例

无法创建一个类型参数的实例,因为类型不确定,下面代码会引起编译时错误。

public static <E> void test(List<E> list){
    E elem = new E();//是错误的
}

通配符从不用作泛型方法调用,泛型类实例创建或超类型的类型实参。

但是,给出Class类型,可以通过反射创建一个参数化类型的实例:

public static <E> void test(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();//正确
}

6.4 没有泛型数组

数组的协变: 如果A的父类是B,则A[]的父类是B[]。

注:不可以创建泛型数组,因为数组是协变的,擦除后就没法满足数组协变的原则。

注:无论A和B两个类是否相关,MyClass<A>和MyClass<B>都没任何关系,他们的公共父对象是Object。

6.5 泛型与异常相关问题

泛型类不能extend ‘java.lang.Throwable’:

image.png

不能捕获泛型类对象:

image.png

但是可以这样写:

public <T extends Exception> void test(T t) throws T {
    try{

    } catch (Exception t1){
        throw t;
    }
}

6.6 类型和子类型

只要不改变类型实参就会在类型之间保留子类型关系。

image.png

  • 28
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值