概述
系列文章目录
第一部分——编程基础与二进制 1
第一部分——编程基础与二进制 2
第二部分——面向对象.类的基础
第二部分——面向对象.类的继承
第二部分——面向对象.类的扩展
第二部分——面向对象.异常
第三部分——泛型与容器.泛型
第三部分——泛型与容器.列表和队列(单独专栏——JDK源码)
第三部分——泛型与容器.Map和Set(单独专栏——JDK源码)
文章目录
- 系列文章目录
- 第三部分——泛型与容器
- 8.泛型
- 8.1基本概念和原理
- 8.1.1一个泛型的案例
- 8.1.2泛型类
- 8.1.3泛型接口
- 8.1.4泛型方法
- 8.1.5类型擦除
- 8.1.6泛型和继承
- 8.1.7类型边界
- 8.2类型通配符
- 8.2.1上界通配符
- 8.2.2下界通配符
- 8.2.3无界通配符
- 8.2.4通配符的约束
- 8.3泛型的细节与局限性
第三部分——泛型与容器
8.泛型
8.1基本概念和原理
8.1.1一个泛型的案例
通过简单的介绍一个Java库中提供给我们的一个容器类——ArrayList,我们将直观的感受泛型的概念以及我们为什么需要泛型
我们之前学过一种数据结构叫数组,数组可以帮我们存储指定个数个元素,通过下标的方式访问,但是使用的时候可能会存在一个问题,就是大小不可变,当我们声明一个数组之后我们将不能改变数组的大小,而在Java中存在这另一个和数组很相似的存储数据用的容器——ArrayList,它除了比数组有更丰富的功能之外还提供了动态扩容的支持,更详细的介绍后面讲容器的章节会继续深入,这里只做大致的说明
具体的使用方式如下:
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
Integer i = list.get(0);
}
我们会发现,创建ArrayList对象和创建之前看过的类的对象没有区别,唯独多了<Integer>
这一部分,而这一部分就是指定了泛型,感性的理解就是我们声明了一个存储Integer也就是int的包装类型的容器,这就是泛型最简单的,也是最重要的功能类型参数化,我们可以像传参一样传入数据类型的信息了
但是,这里就存在一个问题我不指定类型,反正都使用Object接受所有参数,也能实现上面的功能,的确如此
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
list.add(1);
list.add(2);
Integer i = (Integer)list.get(0);
}
这样虽然多了强制转换的麻烦,但是貌似也是能准确运行的,可是如果我像下面这样使用就不一样了
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
list.add(1);
list.add("abc");
list.add(new double[]{1.0, 2.0});
Integer i = (Integer)list.get(1);
}
我们可没有办法保证别人往list里面存入的只有整数,一旦想要通过整型的方式获取其他类型数据会直接报错,数组能通过编译时期的检查保证代码的安全性,同样的对于像这种容器类的对象也需要有方法加以限制,而这个方法也就是通过泛型
至此,我们其实可以理解,泛型无非就是一个特殊方式传入的表达数据类型信息的参数而已,不过这样一个简单的方式却极大的给我们提供了两方面好处:
- 更高的安全性
- 更好的可读性
- 更灵活的代码复用能力
8.1.2泛型类
泛型类定义的语法形式如下:
class name<T1, T2, ..., Tn> {
//代码
}
在类名之后通过<>
将代表类型的标识符包裹,理论上中间的类型标识符就是个代号,取什么字母都可以,但一般有一些约定俗成的规范:
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
下面我们将使用泛型的语法,自定义一个像ArrayList一样可以动态扩容的容器
public class DynamicArray<E> {
private static final int DEFAULT_CAPACITY = 10;
private int size;
private Object[] elementData;
public DynamicArray() {
this.elmentData = 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(elmentData, 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 oldVlaue;
}
}
这样我们就简单地定义了一个使用了泛型的容器,泛型的作用主要体现在添加和获取元素的方法上,用法如下:
DynamicArray<Double> arr = new DynamicArray<>();
arr.add(1.0);
Double e = arr.get(0);
Double old = arr.set(0, 2.0);
同时对于泛型部分也可以出现递归嵌套,如
DynamicArray<Pair<Integer, String>> arr = new DynamicArray<>();
8.1.3泛型接口
接口也是可以定义成泛型的,如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);
}
泛型接口存在两种实现:
- 声明了泛型类型
public final class Integer extends Number implements Comparable<Integer> {
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
}
- 不明确声明泛型类型
public class Content<T> implements Comparable<T> {
public int compareTo(T o) {
//...
}
}
8.1.4泛型方法
泛型方法指引入类型参数的方法,泛型方法可以是普通方法、静态方法以及构造方法
public static <T> int indexOf(T[] arr, T elem) {
for(int i = 0; i < arr.length; i++) {
if(arr[i].equals(elm)) {
return i;
}
}
return -1;
}
对于一个泛型方法我们可以这么调用:
indexOf(new Integer[]{1, 3, 5}, 10);
indexOf(new String[]{"hello", "zhangsan", "lisi"}, "zhangsan");
对于上面方法的调用不像之前一样需要指定类型,因为编译器可以自行推断出泛型类型
同样,泛型方法的类型参数也可以有多个
public static <U, V> Pair<U, V> makePair(U first, V second) {
Pair<U, V> pair = new Pair<>(first, second);
return pair;
}
泛型方法中也可以使用可变参数列表
public static <T> List<T> makeList(T ... args) {
List<T> = list = new ArrayList<T>();
Collection.addAll(list, args);
return result;
}
8.1.5类型擦除
Java语言中泛型的实现方式使用的就是类型擦除,指使用泛型的时候,所有的类型信息都被擦除了
泛型擦除做的工作:
- 把泛型中所有类型参数替换为Object,如果指定了后面介绍的类型边界就使用边界替换
- 擦去出现的类型声明,即去掉<>的内容,同时在必要的地方插入类型转换
- 生成桥接方法保留扩展泛型类型中的多态性
public static void main(String[] args) {
ArrayList<Object> list1 = new ArrayList<Object>();
ArrayList<Integer> list2 = new ArrayList<Integer>();
System.out.println(list1.getClass() == list2.getClass()); //true
}
通过上面这段代码其实可以发现,在编译器的角度不存在ArrayList<Integer>
这种类型,在编译器的角度,他们都是java.util.ArrayList类,这也侧面的反应了类型擦除的过程
Java泛型的实现方式不太优雅,是因为泛型在JDK5时引入的,为了兼容老版本代码,需要在设计上做一定的折中
8.1.6泛型和继承
- 由于类型擦除的存在,泛型不能用于显示的引用运行时类型的操作之中,如转型、instanceof、new等
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6eYrBbfD-1641800520852)(C:UsersmoozleeDesktop桌面文件夹笔记img泛型和继承.png)]
我们可以说Integer
继承了Object
;ArrayList
继承了List
;但是List<Integer>
并不是继承List<Object>
,因此对于看起来可以进行的向上转型是不被允许的
List<Integer> list = new ArrayList<>();
List<Object> list2 = list;// 错误
8.1.7类型边界
- 上界为具体的类
比如,我们想要定义一个makeNumberPair类,比起makePair类我们限定两个类型参数必须是Number或者Number的子类
public static <U extends Number, V extends Number> Pair<U, V> makePair(U first, V second) {
Pair<U, V> pair = new Pair<>(first, second);
return pair;
}
这里我们通过extends
关键字给类型参数提供了上界Number类,限制了传进来的Number的范围,此时类型擦除就不是用Object替换而是通过Number替换
- 上界为某个接口
比如,我们想要实现一个泛型类型作为参数的max方法,可以对于max方法必须要求传入的元素可以比较也就是实现了Comparable接口,那么就可以如下声明
public static <T extends Comparable<T>> 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;
}
对于接口这里同样也是使用关键字extends
,语义上很好理解,类型T
是实现了Comparable<T>
的一个类型,意味着T
类型的变量可以与其他T
类型对象比较
- 上界有多个
<T extends B1 & B2 & B3>
当想要设置多个上界时,可以使用&
将多个上界隔开,其中extends后面接的第一个参数可以是类或者接口,其余都只能是接口,这也好理解,因为Java不允许多继承可以多实现且extends关键字要在implements关键字之前
8.2类型通配符
类型通配符通俗点讲就是声明一个基于某泛型类型的约束条件
说明一点,类型通配符和类型边界是完全不同的两个东西,一个用于声明类型,一个用于声明约束,具体的不同下面可以慢慢体会
8.2.1上界通配符
当我们仅仅只需要声明一种约束,而不是定义一个类型出来时,我们就可以使用?
代替我们需要单独定义出来配合的类型参数,如在DynamicArray中
public void addAll(DynamicArray<? extends E> c) {
for(int i = 0; i < c.size; i++) {
add(c.get(i));
}
}
上面这段代码中,我们要求传入addAll方法的参数是一个存储E或E的子类的对象的DynamicArray,这里我们就只需要使用<? extends E>
来声明一种约束,没有必要再定义一个方法泛型参数T来配合E声明约束
<T extends E>
与<? extends E>
的不同点的关键就在于:
<T extends E>
定义了一个有约束的类型参数T,T可以在后续出现<? extends E>
之声明了一个对于类型参数的要求
对于addAll的例子,以下的这两种声明都是可以的:
public void addAll(DynamicArray<? extends E> c)
public <T extends E> void addAll(DynamicArray<T> c)
只不过后者是声明了一种带有约束的类型T给DynamicArray<T>
使用,以此来声明约束,比起前者直接声明约束就显得很繁琐了
8.2.2下界通配符
约束有上界,那么也会有下界,我们可以指定一种是某个类或其超类的类型的约束
看个例子,如过我们需要给DynamicArray添加一个方法:
public void copyTo(DynamicArray<E> dest) {
for(int i = 0; i < size; i++) {
dest.add(get(i));
}
}
如果像上面这么定义,那么对于以下这种使用可能就不太满足:
DynamicArray<Integer> ints = new DynamicArray<Integer>();
ints.add(10);
ints.add(34);
DynamicArray<Number> numbers = new DynamicArray<Number>();
ints.copyTo(numbers);
想“换个大一点的碗装”的需求是可以理解的,但是如果使用之前那种定义方法这样使用方法的参数类型检查就会不通过,因此我们就需要介入下界通配符
public void copyTo(DynamicArray<? super E> dest) {
for(int i = 0; i < size; i++) {
dest.add(get(i));
}
}
这样的定义方式上述代码运行就不会报错,感性认识就是这个方法支持并认同Java中大类型的引用可以指向小类型对象的特质
8.2.3无界通配符
无界通配符有两种应用场景:
- 只需要使用Object类提供的功能
- 使用不依赖于类型参数的泛型类中的方法
如:
public static int indexOf(DynamicArray<?> arr, Object elm) {
for(int i = 0; i < arr.size(); i++) {
if(arr.get(i).equals(elm)) {
return i;
}
}
return -1;
}
在上面的方法中,可以一个方法适配存储所有类型的DynamicArray,因为在方法中我们只需要使用元素equals方法即可
8.2.4通配符的约束
- 上界和无界通配符只能读,不能写
DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Integer a = 200;
numbers.add(a);//错误
numbers.add((Number)a);//错误
numbers.add((Object)a);//错误
有这个规定很好理解,对于一个类型不确定的元素,允许写入会威胁到类型安全性,但是在某些合理的场景下,我们的确需要写入,如:
public static void swap(DynamicArray<?> arr, int i, int j) {
Object tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
}
这样写的话,两条set语句全部都是非法的,但是我们可以借助带类型参数的泛型方法解决
public static <T> void swapInternal(DynamicArray<T> arr, int i, int j) {
T tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
}
public static void swap(DynamicArray<?> arr, int i, int j) {
swapInternal(arr, i, j);
}
这样通过指定类型的泛型函数就解决了这样的问题
- 泛型不能向上转型但是,通过通配符可以
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList;// 错误
List<? extends Integer> intList2 = new ArrayList<>();
List<? extends Number> numList2 = intList2// OK
8.3泛型的细节与局限性
- 基本类型不能用于实例化类型参数
List<int> list = new ArrayList<int>();// 错误
我们需要使用这些基本数据类型的包装类型
- 运行时类型信息不适用于泛型
在前面8.1.6中提到过这个性质,必要时候可以使用通配符代替
- 由于类型擦除,可能接口的实现存在一些冲突
当我们定义一个父类
class Base implements Comparable<Base>
一个子类
class Child extends Base
其中父类实现了compareTo接口,子类就不需要自己实现,而如果子类希望自定义这个方法呢?
class Child extends Base implements Comparable<Child>
这样定义Java编译器会报错,因为子类父类同时实现了Comparable接口,也就是相当于接口被实现了两次且类型参数不同,因此如果需要修改比较方法就只能重写Base类的实现
class Child extends Base {
public int compareTo(Base o) {
if(!(o instanceof Child)) {
throw new IllegalArgumentException();
}
Child c = (Child) o;
//...
return 0;
}
//...
}
- 由于类型擦除,重载方法存在冲突
public static void test(DynamicArray<Integer> intArr)
public static void test(DynamicArray<String> strArr)
这种重载方式是无效的,因为这两个函数的参数列表在编译器看来仍然没有差别的
- 不能创建类型参数的实例
public static <E> void append(List<E> list) {
E elem = new E(); // 错误
}
如果实在需要使用类型参数创建对象需要使用反射部分的知识,如
public static <T> T create(Class<T> type) {
try {
return type.newInstance();
} catch (Exception e) {
return null;
}
}
Date date = create(Date.class);
StringBuilder sb = create(StringBuilder.class);
- 泛型类的类型参数不能用于静态变量和方法
这里说的是泛型类的类型参数,对于泛型方法是可以为静态的
public class Sigleton<T> {
private static T instance; // 错误
public synchronized static T getInstace() { // 错误
}
}
上面这两个写法,都是编译不通过的
- 不能创建泛型数组
List<Integer>[] array = new List<Integer>[2]; //错误
- 不能创建、catch、throw参数化类型对象
class MathException<T> extends Exception { /* ... */ }
// 编译错误
class QueueFullException<T> extends Throwable { /* ... */ // 编译错误
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) {
// compile-time error
// ...
}
}
最后
以上就是英俊画板为你收集整理的第三部分——泛型与容器.泛型系列文章目录第三部分——泛型与容器的全部内容,希望文章能够帮你解决第三部分——泛型与容器.泛型系列文章目录第三部分——泛型与容器所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复