本文基于廖雪峰老师的网站进行学习,可以当作二次解读,因此可能存在多数内容引用自廖老师的网站:
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 {
private T[] array;
private int size;
public void add(T e) {...}
public void remove(int index) {...}
public T get(int index) {...}
}
我们一般使用 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 {
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 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类型。
那么这种方式将会产生一些不是很好的事情。
这点开始也提到了,原因就是因为泛型在虚拟机中是以Object的形式执行的,而基本数据类型并不是Object的子类,在编译器中无法实现最后的强制类型转换。
通过getclass()方法只能获取到当前逻辑方法的类名,例如上面的 Pair
只能得到 class Pair,而不会携带
,因此无法获知泛型是String或是其它。
这个应该比较好理解,毕竟类名是确定的,泛型只是类里字段或方法的类型,而类名并不会因为类内部的字段类型的改变而改变,也因为虚拟机实现都是Object,所以它返回的总是同一个类。
在泛型类的内部,虽然T是代表一种类,但是不能用T来创建一个新的实例。
public class Pair{
...
# 无法 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 {
private T first;
private T last;
public Pair(Class clazz) {
first = clazz.newInstance();
last = clazz.newInstance();
}
}
使用的时候:
Pair pair = new Pair<>(String.class);
public class Pair {
public boolean equals(T t) {
return this == t;
}
}
equals
方法无法通过编译,因为有一个 equals
方法是继承自 Object 的,编译器会阻止一个实际上会变成覆写的泛型方法定义,换另外一个名字即可,尽量避免自定义的方法名和常用的方法名同名。
泛型可以实现接口,接口中也可以使用泛型。
这句话有两个泛型,需要注意区分概念。泛型其实指的是一种将类型也作为输出参数的一种方法思想,而一般情况下我们说的泛型是指使用了泛型的普通class,如这句话的第一个泛型;第二个泛型指的的思想,将这种泛型的思想应用在接口上。
例如 ArrayList
就实现了 list
接口:
public class ArrayList implements List {
...
}
# 向上转型
List list = new ArrayList();
并且泛型也可以向上转型,也就是开始说的实例是 ArrayList
类型(需要再次注意,你 getclass 时,只能得到 ArrayList 而没有后面的 String),但是可以变更为父类的声明。
并且需要注意 ArrayList
和 ArrayList
或者其它什么的并不存在继承关系,也无法进行转型。也就是说泛型 <>
里的继承关系,和实现了泛型的类之间没有关系。
并不是只有 class 才可以使用泛型,泛型思想并不挑食,有将类型作为参数的需求,就可以用上泛型。
比如 Comparable
这个泛型接口:
public interface Comparable {
/**
* 返回-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 {
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 {
}
此时的子类 StrPair
和普通 class 没有区别,它不再具有泛型的特征,但是子类可以获取泛型的类型
,方法比较复杂就不说了。
当然泛型也可以继承自泛型,因为本质上它是实现了泛型的普通类,和普通类的写法差不离。
泛型类中静态方法的定义和其它方法的定义略有区别,这和静态方法的初始化时间有关系,静态方法总在类加载时完成初始化,而其它方法需要在创建实例时完成初始化。而我们知道泛型需要在创建实例的时候才会传入具体的类型参数,因此静态方法完成初始化时并不能知道是什么类型。
我们需要这样定义静态方法:
public class Pair {
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) {
......
}
}
这样定义无法通过编译:
public class Pair {
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 后面需要添加一个泛型的标志
,并且静态方法和普通的方法的标志也要区分开来。
个人认为:
静态方法是和类一起完成初始化的,这里我们可以把它等同于一个特殊的类,而类名后面需要添加
以申明这是一个泛型,那么静态方法同样需要添加一个
申明这是一个泛型,并且静态方法的存储区域是特殊的,因此泛型
和所在类的
是不一样的,为了区分这种差别我们选用另外一个
来表示(继续用 T 也是可以的,但是要记得此 T 不同彼 T)。
另外由此也能看出来,一个泛型内其实是支持多种不同的参数输入的,以上面 create(K first, K last)
为例,如果 first 和 last 类型不一致时,可以选用不同的字母区分开来 create(K first, V last)
, 然后在类名上也需要做出体现 public class Pair
。
我们已经见过了泛型的实现原理和泛型类的编写实现。那么一个实例化的泛型在其它的类中是怎么使用的呢?又有哪些需要注意的?
对于这样一个泛型类 Pair
:
class Pair {
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 p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
}
// 新建一个静态方法调用泛型实例
static int add(Pair p) {
Number first = p.getFirst();
Number last = p.getLast();
return p.getFirst().intValue() + p.getFirst().intValue();
}
这就是最基本的调用方法,只不过这样每次实例的泛型类型改变,调用该实例的方法的传入类型也要发生改变:static int add(Pair
。
为了使得方法能够更加健壮一点,我们这么修改一下这个静态方法:
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 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 { ... }
这样泛型的传入参数就只能是 Number 的子类而非 Object 的子类,并且在虚拟机中,T 会被擦拭成 Number 而非 Object。