本节我们主要来介绍泛型的基本概念和原理

后续章节我们会介绍各种容器类,容器类可以说是日常程序开发中天天用到的,没有容器类,难以想象能开发什么真正有用的程序。而容器类是基于泛型的,不理解泛型,我们就难以深刻理解容器类。那,泛型到底是什么呢?

什么是泛型?

一个简单泛型类

我们通过一个简单的例子来说明泛型类的基本概念、实现原理和好处。

基本概念

我们直接来看代码:

public class Pair<T> {

    T first;
   
 T second; public Pair(T first, T second){
        
        this.first = first; this.second = second;
    
     } public T getFirst() {
          
         return first;
   
     } public T getSecond() { 
         
         return second;
    }
}

 

Pair就是一个泛型类,与普通类的区别,体现在:

类名后面多了一个 
first和second的类型都是T 
T是什么呢?T表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。

怎么用这个泛型类,并传递类型参数呢?看代码:

Pair<Integer> minmax = new Pair<Integer>(1,100);
 Integer min = minmax.getFirst();
 Integer max = minmax.getSecond();

 

Pair< Integer >,这里Integer就是传递的实际类型参数。

Pair类的代码和它处理的数据类型不是绑定的,具体类型可以变化。上面是Integer,也可以是String,比如:

Pair<String> kv = new Pair<String>("name","老马");

 

类型参数可以有多个,Pair类中的first和second可以是不同的类型,多个类型之间以逗号分隔,来看改进后的Pair类定义:

public class Pair<U, V> {

    U first;
    V second; public Pair(U first, V second){ this.first = first; this.second = second;
    } public U getFirst() { return first;
    } public V getSecond() { return second;
    }
}

 

可以这样使用:

Pair<String,Integer> pair = new Pair<String,Integer>("老马",100);

 

< String,Integer >既出现在了声明变量时,也出现在了new后面,比较啰嗦,Java支持省略后面的类型参数,可以这样:

Pair<String,Integer> pair = new Pair<>("老马",100);

 

基本原理

泛型类型参数到底是什么呢?为什么一定要定义类型参数呢?定义普通类,直接使用Object不就行了吗?比如,Pair类可以写为:

public class Pair {

    Object first;
    Object second; public Pair(Object first, Object second){ this.first = first; this.second = second;
    } public Object getFirst() { return first;
    } public Object getSecond() { return second;
    }
}

 

使用Pair的代码可以为:

Pair minmax = new Pair(1,100);
 Integer min = (Integer)minmax.getFirst();
 Integer max = (Integer)minmax.getSecond();
Pair kv = new Pair("name","老马"); 
String key = (String)kv.getFirst(); 
String value = (String)kv.getSecond();

这样是可以的。实际上,Java泛型的内部原理就是这样的。

我们知道,Java有Java编译器和Java虚拟机,编译器将Java源代码转换为.class文件,虚拟机加载并运行.class文件。对于泛型类,Java编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通Pair类代码及其使用代码一样,将类型参数T擦除,替换为Object,插入必要的强制类型转换。Java虚拟机实际执行的时候,它是不知道泛型这回事的,它只知道普通的类及代码。

再强调一下,Java泛型是通过擦除实现的,类定义中的类型参数如T会被替换为Object,在程序运行过程中,不知道泛型的实际类型参数,比如Pair,运行中只知道Pair,而不知道Integer,认识到这一点是非常重要的,它有助于我们理解Java泛型的很多限制。

Java为什么要这么设计呢?泛型是Java 1.5以后才支持的,这么设计是为了兼容性而不得已的一个选择。

泛型的好处

既然只使用普通类和Object就是可以的,而且泛型最后也转换为了普通类,那为什么还要用泛型呢?或者说,泛型到底有什么好处呢?

主要有两个好处:

  • 更好的安全性
  • 更好的可读性

语言和程序设计的一个重要目标是将bug尽量消灭在摇篮里,能消灭在写代码的时候,就不要等到代码写完,程序运行的时候。

只使用Object,代码写错的时候,开发环境和编译器不能帮我们发现问题,看代码:

Pair pair = new Pair("老马",1); Integer id = (Integer)pair.getFirst(); String name = (String)pair.getSecond();

 

看出问题了吗?写代码时,不小心,类型弄错了,不过,代码编译时是没有任何问题的,但,运行时,程序抛出了类型转换异常ClassCastException。

如果使用泛型,则不可能犯这个错误,如果这么写代码:

Pair<String,Integer> pair = new Pair<>("老马",1); Integer id = pair.getFirst(); String name = pair.getSecond();

 

开发环境如Eclipse会提示你类型错误,即使没有好的开发环境,编译时,Java编译器也会提示你。这称之为类型安全,也就是说,通过使用泛型,开发环境和编译器能确保你不会用错类型,为你的程序多设置一道安全防护网。

使用泛型,还可以省去繁琐的强制类型转换,再加上明确的类型信息,代码可读性也会更好。

容器类

泛型类最常见的用途是作为容器类,所谓容器类,简单的说,就是容纳并管理多项数据的类。数组就是用来管理多项数据的,但数组有很多限制,比如说,长度固定,插入、删除操作效率比较低。计算机技术有一门课程叫数据结构,专门讨论管理数据的各种方式。

这些数据结构在Java中的实现主要就是Java中的各种容器类,甚至,Java泛型的引入主要也是为了更好的支持Java容器。后续章节我们会详细讨论主要的Java容器,本节我们先自己实现一个非常简单的Java容器,来解释泛型的一些概念。

我们来实现一个简单的动态数组容器,所谓动态数组,就是长度可变的数组,底层数组的长度当然是不可变的,但我们提供一个类,对这个类的使用者而言,好像就是一个长度可变的数组,Java容器中有一个对应的类ArrayList,本节我们来实现一个简化版。

来看代码:

 1 public class DynamicArray<E> {
 2     private static final int DEFAULT_CAPACITY = 10; 
 3     private int size;
 4     private Object[] elementData;
 5  public DynamicArray() { 
 6      this.elementData = new Object[DEFAULT_CAPACITY];
 7  } 
 8 private void ensureCapacity(int minCapacity) { 
 9      int oldCapacity = elementData.length;   
10 
11      if(oldCapacity>=minCapacity){ 
12       
13       return;
14         } 
15      int newCapacity = oldCapacity * 2;
16       if (newCapacity < minCapacity)
17             newCapacity = minCapacity;
18         elementData = Arrays.copyOf(elementData, newCapacity);
19     } public void add(E e) {
20         ensureCapacity(size + 1);
21         elementData[size++] = e;
22     } public E get(int index) { return (E)elementData[index];
23     } public int size() { return size;
24     } public E set(int index, E element) {
25         E oldValue = get(index);
26         elementData[index] = element; return oldValue;
27     }
28 
29 }
View Code

相关文章: