Java.泛型相关

泛型

本文基于廖雪峰老师的网站进行学习,可以当作二次解读,因此可能存在多数内容引用自廖老师的网站:
https://www.liaoxuefeng.com/wiki/1252599548343744/1255945193293888
本文仅用作个人学习的记录,包含个人学习过程的一些思考,想到啥写啥,因此有些东西阐述的很罗嗦,逻辑可能也不清晰,看不懂的且当作是作者的呓语,自行跳过即可。

它叫泛型

在Java中,我们定义一个方法或者字段的时候,总需要预先申明方法的返回值类型、字段的类型(构造方法除外,构造方法不需要申明返回类型),而其实该方法可能适用于多种不同的输入类型和返回类型:

public class ArrayList {
    private Object[] array;
    private int size;
    public void add(Object e) {...}
    public void remove(int index) {...}
    public Object get(int index) {...}
}

例如上面定义的 ArrayList 类,对于其中的字段 array 申明成 Object[] 类型,对于其中定义的方法的输入(出)参数申明为Object类型,(当然这里还有int定义的size、index,但是这个是固定的,对于别的类型输入也不需要更改,但是你要更改也可以,这个之后再说),这些换成其他类型例如 String,这个类和方法也是可以编译运行的。

这种仅仅需要更改参数类型(方法签名)而不更改代码逻辑,我们如果要重新写另外一份代码就比较繁琐,不符合能复用就复用的原则,因此就引入了泛型的概念。额外插一句,学到这总给我一种熟悉的感觉,摩挲着略有扎人的下颚,捻了捻稀疏的胡髭,嗯,是重载的感觉没跑了。重载是根据不同的输入(包括类型和个数)从而选择不同逻辑的同名方法,而泛型只是对不同类型的输入(大多数情况是输入,也不一定是输入,其实还可以是其它的)选择一套相同的逻辑方法。提到了重载就再复习一下覆写的概念,覆写存在于继承关系中,子类重新定义父类已有的方法(方法签名、输入类型个数都相同)。

扯远了,回来讲泛型。泛型和方法本身有点像,方法是针对不同数值的输入,匹配相同的逻辑运算,泛型在这个基础上把输入的类型也算成一个输入变量:

public class ArrayList<T> {
    private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

我们一般使用 T 来表示这个未确定的类型变量,在类名后面加 <T> 表示这是个泛型类,T可以是任何类型或者说class,但是不包括所有的基本类型 {byte, short, int, long, float, double, char, boolean},可以是它们对应的引用类型 {Byte, Short, Integer, Long, Float, Double, Character, Boolean}。

一个叫擦拭的方法和它的局限性

在Java中,泛型是通过擦拭法(Type Erasure)实现的。

所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

虽然从上面这个定义没看出来擦拭二字体现在何处,且放在一边。

对于泛型,T 可以是任意class,也就是说对于虚拟机运行而言,这是一个未知的参数,这在Java里是不被允许的。因此在虚拟机中执行时,并不存在泛型,所有的 T 都被视为 Object,因为所有的 class 都继承自 Object。然后在最后由编译器中,针对实际使用的 T (这里是确定的class,而不是泛指),再对返回值进行强制的类型转换。

这是编译器得到的代码:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

这是虚拟机得到的代码:

public class Pair {
    private Object first;
    private Object last;
    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }
    public Object getFirst() {
        return first;
    }
    public Object getLast() {
        return last;
    }
}

在实际执行过程中,编译器:

Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

而虚拟机:

Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

所以这个泛型的实现并不像我们最开始认知那样,实际使用时将实际的类型代入整个逻辑结构实现;而是从侧面先用Object类完成整个逻辑运算,再用实际的使用类型以子类强制转换父类的方式,擦拭掉Object类型。

那么这种方式将会产生一些不是很好的事情。

  1. T不能是基本数据类型

这点开始也提到了,原因就是因为泛型在虚拟机中是以Object的形式执行的,而基本数据类型并不是Object的子类,在编译器中无法实现最后的强制类型转换。

  1. 无法获取和判断T的类型

通过getclass()方法只能获取到当前逻辑方法的类名,例如上面的 Pair<String> 只能得到 class Pair,而不会携带 <String>,因此无法获知泛型是String或是其它。
这个应该比较好理解,毕竟类名是确定的,泛型只是类里字段或方法的类型,而类名并不会因为类内部的字段类型的改变而改变,也因为虚拟机实现都是Object,所以它返回的总是同一个类。

  1. T不能实例化

在泛型类的内部,虽然T是代表一种类,但是不能用T来创建一个新的实例。

public class Pair<T>{
	...
	# 无法 new T()
	private T first = new T();
	private T last;
	...
	pubilc Pablic() {
		# 这样同样也是无法 new T()
		this.last = new T();
	}
}

个人浅谈一下对这个问题的理解,不一定正确:
开始已经说过了,在虚拟机中所有的 T 都会转换为 Object 去完成逻辑部分。因此,这里的 new T() 会被视为 new Object(), 也就是创建的是一个 Object 实例,而在最后编译器是需要做强制类型转换的,父类实例是无法转换成子类实例的。

那可能有同学会疑惑,开始讲泛型讲擦拭法不就是要将父类强制转换成它的子类吗?为什么这里又说不能转换呢?

这里需要弄清楚两个概念,声明实例。我们开始讲的泛型、擦拭法都是将字段、方法声明成 T 类型,也就是虚拟机中的 Object 类型。我们知道子类是可以转变成父类类型的,比如任何class都是可以赋值给Object类的,变成Object类型,但是它本质实例还是子类类型,它还可以重新转换成原先的子类类型。但是一个实例化是Object类型的字段,你是没办法将它声明成它的子类类型的。

# 实例化
Object a = new Object();
String b = "123";

# 赋值给声明变量
Object c = b; // OK!
String d = (String) c; // OK!
String e = (String) a; // error!

那么要如何在泛型中完成这个实例化的操作呢?需要将实际类型作为参数传入进去,然后借助静态工厂方法创建实例。

public class Pair<T> {
    private T first;
    private T last;
    public Pair(Class<T> clazz) {
        first = clazz.newInstance();
        last = clazz.newInstance();
    }
}

使用的时候:

Pair<String> pair = new Pair<>(String.class);
  1. 方法定义需要注意命名
public class Pair<T> {
    public boolean equals(T t) {
        return this == t;
    }
}

equals 方法无法通过编译,因为有一个 equals 方法是继承自 Object 的,编译器会阻止一个实际上会变成覆写的泛型方法定义,换另外一个名字即可,尽量避免自定义的方法名和常用的方法名同名。

继承泛型

泛型可以实现接口,接口中也可以使用泛型。
这句话有两个泛型,需要注意区分概念。泛型其实指的是一种将类型也作为输出参数的一种方法思想,而一般情况下我们说的泛型是指使用了泛型的普通class,如这句话的第一个泛型;第二个泛型指的的思想,将这种泛型的思想应用在接口上。

它实现了接口

例如 ArrayList 就实现了 list 接口:

public class ArrayList<T> implements List<T> {
    ...
}
# 向上转型
List<String> list = new ArrayList<String>();

并且泛型也可以向上转型,也就是开始说的实例是 ArrayList<String> 类型(需要再次注意,你 getclass 时,只能得到 ArrayList 而没有后面的 String),但是可以变更为父类的声明。

并且需要注意 ArrayList<String>ArrayList<Number> 或者其它什么的并不存在继承关系,也无法进行转型。也就是说泛型 <> 里的继承关系,和实现了泛型的类之间没有关系。

接口要用它

并不是只有 class 才可以使用泛型,泛型思想并不挑食,有将类型作为参数的需求,就可以用上泛型。
比如 Comparable<T> 这个泛型接口:

public interface Comparable<T> {
    /**
     * 返回-1: 当前实例比参数o小
     * 返回0: 当前实例与参数o相等
     * 返回1: 当前实例比参数o大
     */
    int compareTo(T o);
}

使用:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        Person[] ps = new Person[] {
            new Person("Bob", 61),
            new Person("Alice", 88),
            new Person("Lily", 75),
        };
        Arrays.sort(ps);
        System.out.println(Arrays.toString(ps));
    }
}

class Person implements Comparable<Person> {
    String name;
    int score;
    Person(String name, int score) {
        this.name = name;
        this.score = score;
    }
    @Override
    public int compareTo(Person other) {
        return this.name.compareTo(other.name);
    }
    public String toString() {
        return this.name + "," + this.score;
    }
}

想要调用 Arrays.sort(Object[]) 对任意数组进行排序,待排序的元素必须实现 Comparable 这个泛型接口。

普通类继承自泛型

一个类可以继承自泛型类,但是需要注意的是需要给定泛型的类型。

public class StrPair extends Pair<String> {
}

此时的子类 StrPair 和普通 class 没有区别,它不再具有泛型的特征,但是子类可以获取泛型的类型 <String>,方法比较复杂就不说了。
当然泛型也可以继承自泛型,因为本质上它是实现了泛型的普通类,和普通类的写法差不离。

泛型中的静态方法

泛型类中静态方法的定义和其它方法的定义略有区别,这和静态方法的初始化时间有关系,静态方法总在类加载时完成初始化,而其它方法需要在创建实例时完成初始化。而我们知道泛型需要在创建实例的时候才会传入具体的类型参数,因此静态方法完成初始化时并不能知道是什么类型。

我们需要这样定义静态方法:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    public static <K> void create(K first, K last) {
        ......
    }
}

这样定义无法通过编译:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    public static void create(K first, K last) {
        ......
    }
}

static 后面需要添加一个泛型的标志 <K>,并且静态方法和普通的方法的标志也要区分开来。

个人认为:
静态方法是和类一起完成初始化的,这里我们可以把它等同于一个特殊的类,而类名后面需要添加 <T> 以申明这是一个泛型,那么静态方法同样需要添加一个 <T> 申明这是一个泛型,并且静态方法的存储区域是特殊的,因此泛型 <T> 和所在类的 <T> 是不一样的,为了区分这种差别我们选用另外一个 <K> 来表示(继续用 T 也是可以的,但是要记得此 T 不同彼 T)。

另外由此也能看出来,一个泛型内其实是支持多种不同的参数输入的,以上面 create(K first, K last) 为例,如果 first 和 last 类型不一致时,可以选用不同的字母区分开来 create(K first, V last), 然后在类名上也需要做出体现 public class Pair<T, V>

使用实例化的泛型

我们已经见过了泛型的实现原理和泛型类的编写实现。那么一个实例化的泛型在其它的类中是怎么使用的呢?又有哪些需要注意的?

对于这样一个泛型类 Pair<T>

class Pair<T> {
    private T first;
    private T last;
    
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

我们在另外一个class使用它,假设是Main:

public class Main {
    public static void main(String[] args) {
    	// 新建一个 Pair 实例
        Pair<Number> p = new Pair<>(123, 456);
        int n = add(p);
        System.out.println(n);
    }
	
	// 新建一个静态方法调用泛型实例
    static int add(Pair<Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return p.getFirst().intValue() + p.getFirst().intValue();
    }

这就是最基本的调用方法,只不过这样每次实例的泛型类型改变,调用该实例的方法的传入类型也要发生改变:static int add(Pair<Number> p)

为了使得方法能够更加健壮一点,我们这么修改一下这个静态方法:

static int add(Pair<? extends Number> p) {
    Number first = p.getFirst();
    Number last = p.getLast();
    return p.getFirst().intValue() + p.getFirst().intValue();
}

将传入的泛型类型变为 <? extends Number>,那么对于任意的 Number 子类,该方法都是成立的。

对于以下这几种情况,不被允许:

static int add(Pair<? extends Number> p) {
    Integer first = p.getFirst();
    Integer last = p.getLast();
    return p.getFirst().intValue() + p.getFirst().intValue();
}
static int add(Pair<? extends Number> p) {
    Number first = p.getFirst();
    Number last = p.getLast();
    p.setFirst(new Integer(first.intValue() + 100));
    p.setLast(new Integer(last.intValue() + 100));
    return p.getFirst().intValue() + p.getFirst().intValue();
}

原因都是一样的,对于方法 add 来说它内部的 p 的类型是不确定的(只知道是Number的子类),因此无法将它赋值给确定的子类类型 Integer,调用的 set 方法也无法传入确定子类实例。

<? extends Number> 这种形式的方法,无法对泛型内的方法进行传入参数,也就是这里的 add 没法子调用 setFirst 和 setLast 方法。或者说在 add 方法里,它对于泛型实例是一个只读的权限,而没办法写入。
这并不是说我们没法调用 set 方法,我们在 main 函数中还是可以操作的。

    public static void main(String[] args) {
    	// 新建一个 Pair 实例
        Pair<Number> p = new Pair<>(123, 456);
        p.setFirst(new Integer(first.intValue() + 100));
        System.out.println(p.getFirst());  // 223 
    }

因为对于 main 来说,此时的 p 类型是确定的,所以传入 Integer 类型的实例是没有问题的。

由于 <? extends Number> 的只读属性,我们在编写方法时,如果不希望方法会修改泛型的值,我们就可以采用这种方式进行限定。

此外对于泛型的定义也可以采用 <? extends Number> 这种形式,这样可以缩小传入参数的类型。

public class Pair<T extends Number> { ... }

这样泛型的传入参数就只能是 Number 的子类而非 Object 的子类,并且在虚拟机中,T 会被擦拭成 Number 而非 Object。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值