泛型概念
- 泛型(Generics)本质上就是提供类型的“类型参数”,它们也被称为参数化类型(parameterized type)或参量多态(parametric polymorphism);
- 也指具有在多种数据类型上皆可操作的含义;
- 声明时用参数化类型代替,使用时指定具体类型;
- GJ(Generic Java)是对Java语言的一种扩展,用GJ编写的程序看起来和普通Java程序基本相同,只不过多了一些参数化的类型同时少了一些类型转换;
实际上,编译时这些GJ程序也是首先被转化成一般的不带泛型的Java程序后再进行处理的; - 泛型的特点:类型安全、向后兼容、层次清晰、性能收益
泛型类
- 泛型类就是具有一个或多个类型参数的类;
- 类型参数就跟在方法中普通的参数一样。就像一个方法有“形参”来描述它操作的参数的种类,一个泛型也有“形式类型参数”;
- 当一个方法被调用,“实参” 替换“形参”,方法被执行;
- 当一个泛型声明被调用,“实际类型参数” 取代“形式类型参数”;
- 类型参数通常用单个大写字母命名;
一般为:
E表示集合的元素类型,
T表示任意类型,
K,V表示表的键和值;
要注意,泛型参数一定是类类型,不能是基本数据类型;泛型类不能继承Throwable及其子类。
泛型类的定义
public class Structure<E> {
private E element;
public E getElement() {
return element;
}
public void setElement(E element) {
this.element = element;
}
}
public class StructureTest {
public static void main(String[] args) {
Structure structure = new Structure();
structure.setElement(new Object()); //没有指定泛型默认为Object
Structure<String> structure1 = new Structure<String>();
structure1.setElement("Hello");
structure1.setElement(20); // 后期添加的元素的类型与泛型不一致会报编译异常
Structure<int> structure2 = new Structure(); //泛型只支持引用类型,基本类型会报编译错误
Structure<int[]> structure2 = new Structure(); //泛型支持类、接口、数组
}
}
泛型接口
public interface Testable<T> {
T show();
}
public class TestImpl implements Testable<String>{ //可以指定类型
public String show() {
return "Hello!";
}
}
泛型方法
实现类可以继承泛型接口的特性
public class TestImpl<E> implements Testable<E> {
public <T> T show() {
return null;
}
}
泛型接口也可以直接使用
public interface Testable<T> {
//该T只在当前方法内有效,注意类名上的T和其他方法属性上的T与该方法的T无关
<T> T show();
//该方法上的T与其他的T冲突时,生效的是当前方法的T
<T> T show(T t);
}
public class TestImpl implements Testable<Integer>{
public <String> String show() {
return (String) "Hello";
}
public <String> String show(String t) {
return t;
}
}
如何使所有泛型都生效?使其命名不同就可以了。
public interface Testable<E> {
<T> T show(T t,E e);
}
通俗理解泛型
在谈到泛型应用时,我个人遇到使用泛型的场景大都是与集合指定类型相关,因此我有个建议就是查看Java的ArrayList、HashMap等集合的源码,看看他们是怎么写的。
上面的关于泛型的抽象概念你看起来可能一知半解,不是很清晰,不过没关系,下面的3个例子认真看着,要注意注释的内容,它能帮助你快速掌握泛型的使用。
例1:List集合应用泛型
首先我们来谈谈泛型是如何使用的,我拿ArrayList集合举例。
import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo {
public static void main(String[] args) {
//1.ArrayList<E>的使用方式为ArrayList<类型>
//2.List是接口,这意味着除了类,接口也可以使用泛型
//3.当数据类型声明时指定了泛型时,创建对象时就不需要再加泛型了
List<String> list = new ArrayList<>();
//4.当泛型指定了类型,如指定了String类型,那么就只能接收String类型的对象
list.add("Hello");
// list.add(34);//报编译错误
//5.集合中不能存放基本数据类型,因此在指定泛型时,需要使用对应的包装类
// ArrayList<int> list1 = new ArrayList<>();//报编译错误
ArrayList<Integer> list1 = new ArrayList<>();
}
}
例2:树的结点
如果你刷过算法题,我想你一定见到过二叉树,但是像leetcode这样的平台提供的题目,一般都是用int作为结点的value,这样太固化了,不容易使用。
这里我就把value加上泛型,就可以指定对象类型了。
public class TreeNode<T> {
/**
* 在TreeNode指定了类型T,那么类中的T就代表了这个类型
* value是结点中的值
* 这是一个二叉树的结点,所以left和right表示左右叶
*/
private T value;
TreeNode<T> left;
TreeNode<T> right;
public TreeNode(T e) {
this.value = e;
this.left = null;
this.right = null;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
使用TreeNode构建一棵二叉树:
public static void main(String[] args) {
//结点指定为String,还可以为其他类型
TreeNode<String> node1 = new TreeNode<>("Head");
TreeNode<String> node2 = new TreeNode<>("Left");
TreeNode<String> node3 = new TreeNode<>("Right");
node1.left = node2;
node1.right = node3;
}
如果上面的代码你都看懂了,我想你已经理解了泛型的作用了。
例3:手写一个Map集合
public class HashMap<K,V> { //泛型可以指定多个,用逗号分隔
/**
* 定义键值对数组,并初始化容量
* HashMap底层默认容量是16
* HashMap中是依赖Entry对象来存储键值对的
*/
private Entry[] entries = new Entry[10000];
class Entry<K,V>{
K k;
V v;
public Entry(K k,V v){
this.k = k;
this.v = v;
}
}
/**
* 使用hash算法,%
* 通过key的hash值对entries数组长度取余,得到的结果就是该键值对在数组中存储的位置
* @param k
* @param v
*/
public void put(K k,V v){
int index = k.hashCode() % entries.length;
entries[index] = new Entry(k,v);
}
/**
* 通过key的hash值对entries数组长度取余,得到的结果就是该键值对在数组中存储的位置
* 然后直接获取到该位置的value即可
* 这种方式的只需要查询一次,时间复杂度降低到了O(1),效率提高
* @param k
* @return
*/
public V get(K k){
int index = k.hashCode() % entries.length;
return (V)entries[index].v;
}
}
使用自定义的HashMap:
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
map.put(5,"value=5");
System.out.println(map.get(5)); //value=5
}
最后,我们总结,泛型一般应用于指定类中某些变量的数据类型,它解决的是数据类型的安全性问题。
类型通配符
- 类型通配符<?>,一般用于接受使用,但不能够添加;
- List<?>表示元素类型未知的list,它的元素可以匹配任何类型;
- 带通配符的List仅表示它是各种泛型List的父类,并不能把元素添加到其中。
在正常情况下,每种类型只能接受对应类型的泛型
public static void main(String[] args) {
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
printList(stringArrayList);
printList(integerArrayList); //会报错,原因是泛型内的对象类型不匹配
}
public static void printList(List<String> list){
}
如果使用这个通配符,就可以接受任意类型
public static void main(String[] args) {
List<String> stringArrayList = new ArrayList<String>();
printList(stringArrayList);
}
public static void printList(List<?> list){ //<?>表示可以接受任何类型,与(List list)等价
}
另外,该通配符只能访问,不能添加。
public static void printList(List<?> list){
list.add(0); //会报错,原因是不能识别该添加什么类型
Object o = list.get(0); //可以访问,但返回类型是Object类型
}
泛型通配符可以指定接受参数的范围。
public class Father {
}
public class Son extends Father{
}
Father是Son的父类, <? extends Father>
表示只能接受Father类及其Father的子类,其他类都不通过。
public static void main(String[] args) {
ArrayList<Son> list1 = new ArrayList<Son>();
ArrayList<Object> list2 = new ArrayList<Object>();
printList(list1); //通过
printList(list2); //报错
}
public static void printList(List<? extends Father> list){
//表示只能接受Father以及Father子类的类型
}
<? super Son>
表示只能接受Son类及其Son的父类,其他类都不通过。
public static void main(String[] args) {
ArrayList<Son> list1 = new ArrayList<Son>();
ArrayList<Object> list2 = new ArrayList<Object>();
printList(list1); //报错,Son不是Father的父类
printList(list2); //通过,Object是所有类的父类
}
public static void printList(List<? super Father> list){
//表示只能接受Father以及Father子类的类型
}
泛型的擦除机制
当将一个带泛型的集合放到没有泛型的集合中,泛型会被擦除。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
List list1 = list;
list1.add(new Object()); //泛型已经被擦除了
}
泛型只是在编译阶段限制传递的类型,而在运行阶段会擦除掉泛型。