这次简单学习一下Java中的泛型,参考《Java 编程的逻辑》。
基本用法
顾名思义,就是指广泛的类型,通过一个Demo来理解一下:
public class Pair<T>{
T one;
T two;
public Pair(T first,T second) {
this.one = first;
this.two = second;
}
public T getone() {
return one;
}
public T gettwo() {
return two;
}
public static void main(String[] args) {
Pair<Integer> minmax = new Pair<>(1,100);
Integer min = minmax.getone();
Integer max = minmax.gettwo();
System.out.println("Integer one: " + min);
System.out.println("Integer twp: "+ max);
Pair<String> ps = new Pair<>("Andy","23");
System.out.println("String one: " + ps.getone());
System.out.println("String twp: "+ ps.gettwo());
}
}
Pair就是一个泛型类,与普通类的区别在于类后面有个<T>
,并且实例变量one 与 two的类型也是T,T表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以通过参数传入。
Pair<Integer> minmax = new Pair<>(1,100)
;与 Pair<String> ps = new Pair<>("Andy","23")
;就分别传入了Integer与String类型的参数,即代表T为Integer和String类型,可以减少代码重用,运行结果如下:
当然也可以写成两个不同类型的:
public class Pair<T,V>{
T one;
V two;
public Pair(T first,V second) {
this.one = first;
this.two = second;
}
public T getone() {
return one;
}
public V gettwo() {
return two;
}
public static void main(String[] args) {
Pair<Integer,String> pair = new Pair<>(23,"Andy");
System.out.println(pair.getone());
System.out.println(pair.gettwo());
}
}
运行结果如下:
可以给类型参数设定一个上界类型,通过extends关键字实现,比如这里新建Pair的子类并且规定子类类型参数的上界类型为Number,Number的子类就包含我们常见的Integer、Double、Boolean等。
public class Pairchild<T extends Number,V extends Number> extends Pair<T,V>{
public Pairchild(T first, V second) {
super(first, second);
}
public double sum() {
return getone().doubleValue() + gettwo().doubleValue();
}
}
然后在main方法中调用:
public static void main(String[] args) {
Pairchild<Integer,Double> pair = new Pairchild<>(23,13.5);
System.out.println(pair.sum());
}
运行结果如下:
基本原理
定义普通类直接使用Object也行,比如Pair类可以写为:
public class Pair {
Object one;
Object two;
public Pair(Object one, Object two){
this.one = one;
this.two = two;
}
public Object getone() {
return one;
}
public Object gettwo() {
return two;
}
}
这种普通的非泛型代码也是可以的,那为什么一定要定义类型参数呢? 实际上,Java泛型的内部原理就是这样的。Java有Java编译器和Java虚拟机,编译器将Java源代码转换为.class文件,虚拟机加载并运行.class文件。对于泛型而言,Java编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通Pair类代码及其使用代码一样,将类型参数T擦除,替换为Object,插入必要的强制类型转换。因此Java虚拟机实际执行的时候,其实只知道普通的类及代码而不知泛型为何物。具体在这个例子中,Java虚拟机在运行中只知道Pair,而不知道Integer。
这里就有个问题,然只使用普通类和Object就是可以的,而且泛型最后也转换为了普通类,那为什么还要用泛型呢?主要有两个好处:
- 更好的安全性
- 更好的可读性
例如如下代码:
Pair<Integer,String> pair = new Pair<>(23,"Andy");
Integer id = (String)pair.getone();
String name = (Integer)pair.gettwo();
写代码时类型弄错,并且在代码编译时是没有任何问题的,但在运行时,程序就会抛出类型转换异常ClassCastException,而如果使用泛型,则不可能犯这个错误。
容器类
泛型类最常见的用途是作为容器类,所谓容器类,就是容纳并管理多项数据的类。这里利用泛型实现一个简单的动态数组容器,相比底层数组而言动态数组长度可变。当然Java中已经有这样的实现例如ArrayList了,我们目的主要通过泛型来学习实现一个简化版的ArrayList。
public class DynamicArray<E> {
private static final int DEFAULT_CAPACITY = 10;
private int size;
private Object[] elementData;
public DynamicArray() {
this.elementData = new Object[DEFAULT_CAPACITY];
}
private void ensureCapacity(int minCapacity) {
int oldCapacity = elementData.length;
if(oldCapacity>=minCapacity){
return;
}
int newCapacity = oldCapacity * 2;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
public void add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
}
public E get(int index) {
return (E)elementData[index];
}
public int size() {
return size;
}
public E set(int index, E element) {
E oldValue = get(index);
elementData[index] = element;
return oldValue;
}
}
DynamicArray就是一个动态数组,通过ensureCapacity方法来根据需要扩展数组,扩展的主要逻辑是分配一个足够长度的新数组,然后将原内容拷贝到这个新数组中,最后让内部的字符数组指向这个新数组。作为一个容器类,它容纳的数据类型是作为参数传递过来的,例如存放Double类型:
DynamicArray<Double> arr = new DynamicArray<Double>();
Random rnd = new Random();
int size = 1+rnd.nextInt(100);
for(int i=0; i<size; i++){
arr.add(Math.random());
}
Double d = arr.get(rnd.nextInt(size));