Java中的泛型 (上) - 基本概念和原理
本节我们主要来介绍泛型的基本概念和原理
后续章节我们会介绍各种容器类,容器类可以说是日常程序开发中天天用到的,没有容器类,难以想象能开发什么真正有用的程序。而容器类是基于泛型的,不理解泛型,我们就难以深刻理解容器类。那,泛型到底是什么呢?
什么是泛型?
一个简单泛型类
我们通过一个简单的例子来说明泛型类的基本概念、实现原理和好处。
基本概念
我们直接来看代码:
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,本节我们来实现一个简化版。
来看代码:
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就是一个动态数组,内部代码与我们之前分析过的StringBuilder类似,通过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));
这就是一个简单的容器类,适用于各种数据类型,且类型安全。本节后面和后面两节还会以DynamicArray为例进行扩展,以解释泛型概念。
具体的类型还可以是一个泛型类,比如,可以这样写:
DynamicArray<Pair<Integer,String>> arr = new DynamicArray<>()
arr表示一个动态数组,每个元素是Pair< Integer,String >类型。
泛型方法
除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型的,与它所在的类是不是泛型没有什么关系。
我们看个例子:
public static <T> int indexOf(T[] arr, T elm){ for(int i=0; i<arr.length; i++){ if(arr[i].equals(elm)){ return i;
}
} return -1;
}
这个方法就是一个泛型方法,类型参数为T,放在返回值前面,它可以这么调用:
indexOf(new Integer[]{1,3,5}, 10)
也可以这么调用:
indexOf(new String[]{"hello","老马","编程"}, "老马")
与泛型类一样,类型参数可以有多个,多个以逗号分隔,比如:
public static <U,V> Pair<U,V> makePair(U first, V second){ Pair<U,V> pair = new Pair<>(first, second); return pair;
}
与泛型类不同,调用方法时一般并不需要特意指定类型参数的实际类型是什么,比如调用makePair:
makePair(1,"老马");
泛型接口
接口也可以是泛型的,我们之前介绍过的Comparable和Comparator接口都是泛型的,它们的代码如下:
public interface Comparable<T> { public int compareTo(T o);
} public interface Comparator<T> { int compare(T o1, T o2);
boolean equals(Object obj);
}
与前面一样,T是类型参数。实现接口时,应该指定具体的类型,比如,对Integer类,实现代码是:
public final class Integer extends Number implements Comparable<Integer>{ public int compareTo(Integer anotherInteger) { return compare(this.value, anotherInteger.value);
} //... }
通过implements Comparable< Integer >,Integer实现了Comparable接口,指定了实际类型参数为Integer,表示Integer只能与Integer对象进行比较。
再看Comparator的一个例子,String类内部一个Comparator的接口实现为:
private static class CaseInsensitiveComparator implements Comparator<String> { public int compare(String s1, String s2) { //.... }
}
这里,指定了实际类型参数为String。
类型参数的限定
在之前的介绍中,无论是泛型类、泛型方法还是泛型接口,关于类型参数,我们都知之甚少,只能把它当做Object,但Java支持限定这个参数的一个上界,也就是说,参数必须为给定的上界类型或其子类型,这个限定是通过extends这个关键字来表示的。
这个上界可以是某个具体的类,或者某个具体的接口,也可以是其他的类型参数,我们逐个来看下其应用。
上界为某个具体类
比如说,上面的Pair类,可以定义一个子类NumberPair,限定两个类型参数必须为Number,代码如下:
public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> { public NumberPair(U first, V second) { super(first, second);
}
}
限定类型后,就可以使用该类型的方法了,比如说,对于NumberPair类,first和second变量就可以当做Number进行处理了,比如可以定义一个求和方法,如下所示:
public double sum(){ return getFirst().doubleValue()
+getSecond().doubleValue();
}
可以这么用:
NumberPair<Integer, Double> pair = new NumberPair<>(10, 12.34);
double sum = pair.sum();
限定类型后,如果类型使用错误,编译器会提示。
指定边界后,类型擦除时就不会转换为Object了,而是会转换为它的边界类型,这也是容易理解的。
上界为某个接口
在泛型方法中,一种常见的场景是限定类型必须实现Comparable接口,我们来看代码:
public static <T extends Comparable> T max(T[] arr){
T max = arr[0]; for(int i=1; i<arr.length; i++){ if(arr[i].compareTo(max)>0){ max = arr[i];
}
} return max;
}
max方法计算一个泛型数组中的最大值,计算最大值需要进行元素之间的比较,要求元素实现Comparable接口,所以给类型参数设置了一个上边界Comparable,T必须实现Comparable接口。
不过,直接这么写代码,Java中会给一个警告信息,因为Comparable是一个泛型接口,它也需要一个类型参数,所以完整的方法声明应该是:
public static <T extends Comparable<T>> T max(T[] arr){ //... }
< T extends Comparable< T > >是一种令人费解的语法形式,这种形式称之为递归类型限制,可以这么解读,T表示一种数据类型,必须实现Comparable接口,且必须可以与相同类型的元素进行比较。
上界为其他类型参数
上面的限定都是指定了一个明确的类或接口,Java支持一个类型参数以另一个类型参数作为上界。为什么需要这个呢?
我们看个例子,给上面的DynamicArray类增加一个实例方法addAll,这个方法将参数容器中的所有元素都添加到当前容器里来,直觉上,代码可以这么写:
public void addAll(DynamicArray<E> c) { for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
但这么写有一些局限性,我们看使用它的代码:
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);
numbers是一个Number类型的容器,ints是一个Integer类型的容器,我们希望将ints添加到numbers中,因为Integer是Number的子类,应该说,这是一个合理的需求和操作。
但,Java会在number.addAll(ints)这行代码上提示编译错误,提示,addAll需要的参数类型为DynamicArray< Number >,而传递过来的参数类型为DynamicArray< Integer >,不适用,Integer是Number的子类,怎么会不适用呢?
事实就是这样,确实不适用,而且是很有道理的,假设适用,我们看下会发生什么。
DynamicArray<Integer> ints = new DynamicArray<>(); //假设下面这行是合法的 DynamicArray<Number> numbers = ints; numbers.add(new Double(12.34));
那最后一行就是合法的,这时,DynamicArray< Integer >中就会出现Double类型的值,而这,显然就破坏了Java泛型关于类型安全的保证。
我们强调一下,虽然Integer是Number的子类,但DynamicArray< Integer >并不是DynamicArray< Number >的子类,DynamicArray< Integer >的对象也不能赋值给DynamicArray< Number >的变量,这一点初看上去是违反直觉的,但这是事实,必须要理解这一点。
不过,我们的需求是合理的啊,将Integer添加到Number容器中,这没有问题啊。这个问题,可以通过类型限定,这样来解决:
public <T extends E> void addAll(DynamicArray<T> c) { for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
E是DynamicArray的类型参数,T是addAll的类型参数,T的上界限定为E,这样,下面的代码就没有问题了:
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);
对于这个例子,这个写法有点啰嗦,下节我们会看到一种简化的方式。
小结
本节介绍了泛型的基本概念,包括泛型类、泛型方法和泛型接口,关于类型参数,我们介绍了多种上界限定,限定为某具体类、某具体接口、或其他类型参数。泛型类最常见的用途是容器类,我们实现了一个简单的容器类DynamicArray,以解释泛型概念。
在Java中,泛型是通过类型擦除来实现的,它是Java编译器的概念,Java虚拟机运行时对泛型基本一无所知,理解这一点是很重要的,它有助于我们理解Java泛型的很多局限性。
关于泛型,Java中有一个通配符的概念,语法非常令人费解,而且容易混淆,下一节,我们力图对它进行清晰的剖析。
欢迎加群一起学习 574337670
Java中的泛型 (上) - 基本概念和原理的更多相关文章
- Java编程的逻辑 (35) - 泛型 (上) - 基本概念和原理
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java 中的泛型详解-Java编程思想
Java中的泛型参考了C++的模板,Java的界限是Java泛型的局限. 2.简单泛型 促成泛型出现最引人注目的一个原因就是为了创造容器类. 首先看一个只能持有单个对象的类,这个类可以明确指定其持有的 ...
- 【Java入门提高篇】Day14 Java中的泛型初探
泛型是一个很有意思也很重要的概念,本篇将简单介绍Java中的泛型特性,主要从以下角度讲解: 1.什么是泛型. 2.如何使用泛型. 3.泛型的好处. 1.什么是泛型? 泛型,字面意思便是参数化类型,平时 ...
- Java中的泛型 --- Java 编程思想
前言 我一直都认为泛型是程序语言设计中一个非常基础,重要的概念,Java 中的泛型到底是怎么样的,为什么会有泛型,泛型怎么发展出来的.通透理解泛型是学好基础里面中非常重要的.于是,我对<Ja ...
- Java中的泛型 - 细节篇
前言 大家好啊,我是汤圆,今天给大家带来的是<Java中的泛型 - 细节篇>,希望对大家有帮助,谢谢 细心的观众朋友们可能发现了,现在的标题不再是入门篇,而是各种详细篇,细节篇: 是因为之 ...
- Java开发知识之Java中的泛型
Java开发知识之Java中的泛型 一丶简介什么是泛型. 泛型就是指泛指任何数据类型. 就是把数据类型用泛型替代了. 这样是可以的. 二丶Java中的泛型 Java中,所有类的父类都是Object类. ...
- Java 中的泛型
泛型的一般意义: 泛型,又叫 参数多态或者类型参数多态.在强类型的编程语言中普遍作用是:加强编译时的类型安全(类型检查),以及减少类型转换的次数. Java 中的 泛型: 编译时进行 类型擦除 生成与 ...
- 第九节:详细讲解Java中的泛型,多线程,网络编程
前言 大家好,给大家带来详细讲解Java中的泛型,多线程,网络编程的概述,希望你们喜欢 泛型 泛型格式:ArrayList list= new ArrayList(); ArrayList list= ...
- java中的泛型2--注意的一些问题和面试题
前言 这里总结一下泛型中需要注意的一些地方和面试题,通过面试题可以让你掌握的更清楚一些. 泛型相关问题 1.泛型类型引用传递问题 在Java中,像下面形式的引用传递是不允许的: ArrayList&l ...
随机推荐
- 菜鸟学Struts2——零配置(Convention )
又是周末,继续Struts2的学习,之前学习了,Struts的原理,Actions以及Results,今天对对Struts的Convention Plugin进行学习,如下图: Struts Conv ...
- 执行 $Gulp 时发生了什么 —— 基于 Gulp 的前端集成解决方案(二)
前言 文章 在windows下安装gulp —— 基于 Gulp 的前端集成解决方案(一) 中,已经完成对 gulp 的安装,由于是window环境,文中特意提到了可以通过安装 gitbash 来代替 ...
- 在Sublime Text 3上安装代码格式化插件CodeFormatter
1.了解CodeFormatter插件 在Sublime Text 3中编写代码,为了能让我们的代码格式变得漂亮整洁,需要一个能自动格式代码的插件.这里发现CodeFormatter插件不错,它能支持 ...
- nodejs进阶(2)—函数模块调用
函数调用 1. 文件内普通函数调用 创建一个js文件命名为2_callFunction.js,其中定义一个函数fun1,向返回对象输出了一段字符串“你好,我是fun1”. //------------ ...
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(70)-微信公众平台开发-成为开发者
系列目录 前言: 一.阅读这段系列之前,你必须花半天时间大致阅读微信公众平台的API文档,我尽量以简短快速的语言与大家分享一个过程 二.借助微信公众平台SDK Senparc.Weixin for C ...
- 《LoadRunner12七天速成宝典》来了
看到自己的新书又要发行了,算算从09年第一本书开始,不知不觉已经是第四本书了(帮朋友合写的书不算),每次写完之后都会说太累了,不想再写了,但是却又次次反悔,吞下食言的苦果.如果非要说第四本书的感受,那 ...
- 自己写的数据交换工具——从Oracle到Elasticsearch
先说说需求的背景,由于业务数据都在Oracle数据库中,想要对它进行数据的分析会非常非常慢,用传统的数据仓库-->数据集市这种方式,集市层表会非常大,查询的时候如果再做一些group的操作,一个 ...
- JavaScript事件代理和委托(Delegation)
JavaScript事件代理 首先介绍一下JavaScript的事件代理.事件代理在JS世界中一个非常有用也很有趣的功能.当我们需要对很多元素添加事件的时候,可以通过将事件添加到它们的父节点而将事件委 ...
- 微信小程序开发日记——高仿知乎日报(上)
本人对知乎日报是情有独钟,看我的博客和github就知道了,写了几个不同技术类型的知乎日报APP 要做微信小程序首先要对html,css,js有一定的基础,还有对微信小程序的API也要非常熟悉 我将该 ...
- 太多选择——企业如何选择合适的BI工具?
在没认清现状前,企业当然不能一言不合就上BI. BI不同于一般的企业管理软件,不能简单归类为类似用于提高管理的ERP和WMS,或用于提高企业效率的OA.BPM.BI的本质应该是通过展现数据,用于加强企 ...