Java笔记(五)泛型

时间:2023-03-08 19:29:17
Java笔记(五)泛型

泛型

一、基本概念和原理

泛型将接口的概念进一步延申,“泛型”的字面意思是广泛的类型。

类、接口和方法都可以应用于非常广泛的类型,代码与它们能够操作

的数据类型不再绑定到一起,同一套代码可以应用到多种数据类型。

这样,不仅可以复用代码降低耦合,而且可以提高代码的可读性和安全性。

1.泛型类

public class Pair <T>{
private T first;
private T second; public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}

T表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。

//类型参数可以有多个,用逗号分隔
public class Pair2<T, U> {
private T first;
private U second;
public Pair2(T first, U second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public U getSecond() {
return second;
}
}
        Pair<Integer> pair = new Pair<>(99, 88); //Integer就是传递的实际类型参数
System.out.println(pair.getFirst() + " " + pair.getSecond()); //99 88
Pair2<String, Integer> tang = new Pair2<>("Tang", 22);
System.out.println(tang.getFirst() + " " + tang.getSecond());//Tang 22

2.泛型方法

除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型与它所在的类是不是泛型无关。

public class Alex {
//类型参数位E,放在返回值前面,与泛型类一样类型参数可以有多个
public static <E> int indexOf(E[] arr, E elm) {
for (int i = 0; i < arr.length; i++) {
if (arr[i].equals(elm)) {
return i;
}
}
return -1;
}
public static void main(String[] args) {
int index = indexOf(new Integer[]{1, 2, 3}, 2);
System.out.println(index);//
int tom = indexOf(new String[]{"Tom", "Jimmy", "Cat"}, "Tom");
System.out.println(tom);//
}
}

3.泛型接口

public interface Comparable<T> {
public int compareTo(T o);
}

4.基本原理

Java泛型的内部原理:

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;
}
}

对于泛型类,Java编译器会将泛型代码转换为非泛型代码(Object或者其上边界类型),

就如上面的Pair类代码,将类型参数T擦除,替换为Object,插入必要的强制类型转换。Java

虚拟机执行的时候不知道泛型这回事,只知道普通的类和代码。

Java是从Java5才引入泛型的,所以用类型擦除来实现泛型是一种不得已的选择。

泛型的好处:

编译器帮我们发现代码问题,因此更安全,还提升了代码的可读性。

5.类型参数的限定

Java支持给类型参数设定上界,类型参数必须为给定的上界类型或者其子类型。

//限定类型后如果类型使用错误编译器就会提醒
//并且擦除类型时就会转换为相应的边界类型
public class SonPair<U extends String, V extends Integer> extends Pair<U, V>{
public SonPair(U first, V second) {
super(first, second);
}
}

其中上界可以是某个具体类,也可以是接口或者其他类型参数。

总之,泛型是计算机程序中的一种重要思维方式,它将数据结构和算法与数据类型相分离,

使得同一套数据结构和算法能够应用于各种数据类型,而且可以保证类型安全,提高可读性。

二、通配符

1.更简洁的参数类型限定

1)有限定统配符

形如ClassName<? extends E>这种统配符,叫做有限定统配符匹配E或者E的某个子类,具体的类型未知。

public void addAll(DynamicArray<? extends E> c)
public <T extends E> void addAll(DynamicArray<T> c)

虽然与使用<T extends E>的效果一样,该方法与使用<T extends E>相比不用定义新的类型参数T,

因此也不必把add方法定义为泛型方法,也不用改动相关的源代码。

2)无限定通配符

形如ClassName<?>的统配符为无限定统配符。

比较:

public static int indexOf(DynamicArray<?> arr, Object elm)
public static <T> int indexOf(DynamicArray<T> arr, Object elm)

使用无限定统配符比添加一个泛型参数T更加简洁。

但是,不管是有限定统配符还是无限定统配符都有一个缺陷:只能读,不能写。

例如:

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 <T extends Comparable<T>> T max(DynamicArray<T> arr){
T max = arr.get(0);
for(int i=1; i<arr.size(); i++){if(arr.get(i).compareTo(max)>0){
max = arr.get(i);
}
}
return max;
}
//很难用统配符代替

总之:

1)统配符能做的类型参数都能做

2)因为更简洁,能用通配符就用通配符

3)如果参数类型之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,只能由类型参数。

4)两者往往配合使用

2.超类型通配符

形如<? super E>的通配符被称为超类型通配符,表示E的某个父类型。

使用场景:

1)可以更灵活地写入:

    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(100);
ints.add(34);
DynamicArray<Number> numbers = new DynamicArray<Number>();
ints.copyTo(numbers);//报错,因为期望的类型是DynamicArray<Integer>

解决办法:

    public void copyTo(DynamicArray<? super E> dest){
for(int i=0; i<size; i++){
dest.add(get(i));
}
}

2)解决子类没有实现泛型接口的问题:

public static <T extends Comparable<T>> T max(DynamicArray<T> arr)
    //注意Base实现了Comparable<Base>
class Base implements Comparable<Base>{
//code
}
    //Child并没有实现Comparable<Child>它实现的是Comparable<Base>
class Child extends Base {
//code
}
    DynamicArray<Child> childs = new DynamicArray<Child>();
childs.add(new Child(20));
childs.add(new Child(80));
Child maxChild = max(childs);//编译器报错,提示类型不匹配,

报错原因:在<T extends Comparable<T>> 中对T类型的要求是extends Comparable<T>

而Child并没有实现Comparable<Child>,其实Child也没有必要去重复实现Comparable接口。

解决办法:

public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr)

<? super T>可以配匹Base所以整体是匹配的。

另外,请注意:参数类型限定只有extends形式没有super形式,比如:

<E super T>是有问题的,Java不支持这种语法。

总结:

1)<? super E>用于灵活写入与比较,使得使得父可以使用于子。

2)<?> <? extends E>用于灵活读取。

三、细节和局限性

我们将通过以下几方面介绍这些细节和局限性:

1.使用泛型类、方法和接口

1)因为类型参数会被替换为object或者一个类的父类,所以基本类型不能用来实例化类型参数,解决办法,使用包装类型

2)运行时类型信息不适用于泛型:

在内存中每个类都有一份类型信息,而每个对象也都保存着对应的类型信息的引用,

这个类型信息也是一个对象,它的类型为Class,Class本身也是一个泛型类。

每个类的类型对象可以通过<类名>.class的方式引用,可以通过getClass()方法获得。

这个类型只有一份与泛型无关,所以Java不支持如下写法:

Pair<Integer>.class

一个泛型对象的getClass()方法的返回值与其原始类型对象也是相同的。

同理也不支持:

if (p1 instanceof Pair<Integer>)

不过支持:

if (p2 instanceof Pair<?>)

3)由于类型擦除可能会引发一些冲突:

比如上一个Base与Child的例子,Child没有专门实现Comparable接口

可是如果Child想自己实现接口,重写compareTo()方法呢比如:

class Child extends Base implements Comparable<Child>{
//编译器报错
}

遗憾的是编译器会报错,Comparable接口不能如此被实现两次(一次是Comparable<Base>, 一次是Comparable<Child>)

因为类型擦除后,实际上只能有一个。因此只能通过重写compareTo()方法修改比较方法:

 class Child extends Base {
@Override
public int compareTo(Base o) {
if(!(o instanceof Child)){
throw new IllegalArgumentException();
}
Child c = (Child)o;
return 0;
}
}

4)对重载方法的影响:

public static void test(DynamicArray<Integer> intArr)
public static void test(DynamicArray<String> strArr)//报错

2.定义泛型类、方法和接口

1)不能通过类型参数创建对象,解决方法:使用反射等。

2)泛型类型参数不能用于静态变量和方法,为什么????

3.泛型与数组

1)不能创建泛型数组,例如:

Pair<Object,Integer>[] options = new Pair<Object,Integer>[]{
new Pair("1