泛型编程
参考博客
基本内容
泛型问题:
- 什么是泛型?
- 为什么要引入泛型?
- 泛型的优点在哪里?
推导过程
初始阶段
- 我们在学习数据结构的时候通常会自己手动实现栈,队列,哈希表等数据结构
- 我们通常为了方便起见,都是将数据类型默认设置成为的
Integer
类型 - 那么你是否思考过,如果下次需要将
String
类型的元素存入你写的数据结构要怎么办 - 显然,我们最笨的办法就是再写一个存储
String
类型的数据结构类
// 存储 Integer 元素的类
class StackOfInteger
{
private int[] stack;
private int maxSize;
private int top;
...
}
// 存储 String 元素的类
class StackOfString
{
private String[] stack;
private int maxSize;
private int top;
...
}
...- 那么现在我们再提出一个要求,需要存储
Double
类型的元素的数据结构 - 很无奈,我们只能再次重复一遍数据结构的代码,仅仅只为了改动一个数据类型
中级阶段
- 方式:
- 相对于每次都重写类的方式,利用 Java 多态的特性显然更加符合常理
- 每次都可以任意传入自己想要的类型 -> 所有类都是
Object
子类 -> 所有类型都可以被==向上转型== - 采用
Object
的方式显然避免了代码量过大的问题
class Stack
{
private Object[] stack;
private int maxSize;
private int top;
...// 省略构造方法...
public void push(Object val)
{
if (isFull())
{
System.out.println("栈已满!");
return;
}
stack[++top] = val;
}
public Object pop()
{
if (isEmpty())
{
return null;
}
Object val = stack[top];
top--;
return val;
}
}- 问题:
- 每次取出元素时都需要主动进行==强制类型转换==
- 每个数据结构中都存储同一类型元素(Integer)的时候 -> 将另一类型的元素(String)存储进去也不会报错 -> 强制转换读取元素使才会报错
class MyTest
{
public static void main(String[] args)
{
Stack stack = new Stack(...);
stack.push("String");
// 显然在这个位置想要使用字符串的特性是需要强制转换的
String string = (String) stack.pop();
stack.push(114514);
Integer integer = (Integer) stack.pop();
while(stack.isEmpty())
{
/*
1. 显然如果栈中有其他类型的数据被强制转换成 Integer 类型是显然报错的
2. 我们无法在编译阶段就知道会出现类型转换错误,有时甚至难以得知栈中到底存储的是什么元素
*/
Integer integer = (Integer) stack.pop();
}
}
}- 方式:
最终阶段
- 我们不需要提前指定任何==具体类型== -> 使用==类型参数==来表示我们并不知道现在需要使用哪种具体类型
- 我们只需要在使用数据结构的时候指定具体类型就可以
- 采用这种方式就能够避免 Object 方式中的强制转换造成的错误
class Stack<T>
{
private T[] stack;
private int maxSize;
private int top;
...
public void push(T val)
{
...
}
public T pop()
{
...
}
}
class MyTest
{
public static void main(String[] args)
{
// 后面菱形中不用再填入具体类型 -> 前面已经填入可以自动推导
Stack<String> strStack = new Stack<>();
strStack.push("shinobu");
strStack.push("fuyusakaiori");
// 采用 Object 的方式中是不会在编译阶段报错的 -> 采用泛型编程是会在此处直接报错
strStack.push(1234);
}
}
类型参数:
引入:既然具体变量可以作为实际参数传入,那么是否具体的类型是否也可以作为实际参数传入呢? -> 答案显然是可以的
概念:将类型由原来的==具体的类型参数化==,类似于方法中的变量参数
表现形式:使用<>表示类型参数
- T:表示任何类型
- E(Element):表示集合中的元素类型
- K(Key):表示映射中键的类型
- V(Value):表示映射值的类型
- S(SubType):
总结
最初采用==重写不同类==适应不同类型的需要
改进采用 ==Object 方式(多态)== 适应不同类型的需要
最后采用==泛型==非常容易就适应不同类型的需要
- 概念:泛型的本质是为了==参数化类型== -> 不创建新的类型的情况下,通过泛型指定具体类型来控制形参的具体类型
- 优点:
- 泛型提供一种==扩展能力==,使得数据类别可以像参数一样由外部传递进来,更符合面向抽象开发的软件编程宗旨
- 泛型又提供了一种==类型检测==的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过
- 泛型提高了程序代码的==可读性==,能够一目了然猜测出代码要操作的数据类型
思考:Java 中是否真的实现了泛型机制呢? -> 答案是否定的
/*
1. Java 虚拟机中并没有真正的实现泛型 -> 采用了类型擦除机制 -> 之后解释
2. C# C++ 都是彻底实现了泛型
*/
public static void main(String[] args)
{
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass()); // 得到的答案是 true 而不是 false
}
进阶内容
泛型使用
泛型类
概念:集合框架中对于泛型类的使用非常常见(
ArrayList<T>
,HashMap<K,V>
…)表现形式:使用
类型参数==声明==当前类是泛型类 特点:
泛型类中的构造方法不需要声明泛型
泛型类中的所有方法和属性都可以使用
类型参数 -> 不能够使用未被声明的类型参数,诸如 , …泛型类并不强制要求所有方法和属性都使用
类型参数 泛型类的具体类型的指定
- 可以显示的指定具体的类型
- 也可以不指定具体的类型 -> 这样就可以存放任何类型的元素,不会编译错误 -> 实际上相当于 Object 的方式
总结:==泛型类就相当于普通类的工厂== -> 想要什么样的类就可以生产什么样的类
/* |
泛型接口
- 概念:
- 集合框架中对于泛型接口的使用也非常常见(
List<T>
,Map<K,V>
…) - 泛型接口通常搭配泛型类或者普通类使用
- 集合框架中对于泛型接口的使用也非常常见(
- 表现形式:使用
类型参数==声明==当前接口是泛型接口 - 特点
- 泛型接口的具体类型指定
- 我们可以现在明确指定泛型接口的类型参数
- 我们也可以使用泛型类的类型参数,而不是具体指定一个
- 不能够使用通配符作为类型参数 -> 直接编译报错
- 泛型接口的具体类型指定
interface Generator<T> |
泛型方法
概念:方法前必须使用类型参数
<T>
进行声明才是泛型方法表现形式:定义方法时,在返回值之前声明类型参数
特点
普通泛型方法
- 泛型方法可以将返回值定义为
void
也可以使用<T>
- 泛型方法需要定义自己的类型参数
<T>
- 泛型方法自己的类型参数和泛型类的类型参数==完全不同== -> 即使都使用
<T>
作为类型参数 - 泛型方法中的形参可以使用自己定义的类型参数
<E>
也可以使用泛型类定义的类型参数<T>
- 泛型方法自己的类型参数和泛型类的类型参数==完全不同== -> 即使都使用
- 泛型方法可以将返回值定义为
静态泛型方法
- 静态方法不能够使用泛型类定义的类型参数,只能够使用自己的
class Generic<T> |
泛型限定
概念:对传入类型参数中的具体类型做出限制 -> 不能够传入任意的具体类型
方式
上界
概念:
- 如果限制条件是实体类 -> 传入的具体类型==必须是限制类的子类==
- 如果限制条件是接口 -> 传入的具体类型==必须实现限制接口==
特点:
- 无论限制条件是实体类还是接口 -> 都使用
extends
关键字 - 注:限定条件可以有多个,使用 & 连接;限定类必须写在第一个
- 无论限制条件是实体类还是接口 -> 都使用
测试
- 限定条件是实体类
class Person
{
...
}
class Student extends Person
{
...
}
// 显然只能够是 Person 的子类才能够作为具体类型传入
public static <T extends Person> void genericMethod(T person)
{
System.out.println("泛型测试...");
}
public statci void main(String[] args)
{
genericMethod(new Student());// 合法
genericMethod(new String()); // 不合法 -> 编译阶段就会报错
}- 限定条件是接口
interface PlayGame
{
...
}
class Game implements PlayGame
{
}
// 1. 接口是普通的接口 -> 显然传入的具体类型必须实现这个接口
public static <T extends PlayGame> void genericMethod(T person)
{
System.out.println("泛型测试...");
}
/*
2. 接口是泛型接口
2.1 泛型接口使用类型参数 -> 传入的具体类型只要实现了 Comparator 接口就可以了 -> 从原来的任意范围缩小到有限的范围
2.2 泛型接口使用具体类型 -> 这样写实际上限制死了传入的具体类型 -> 从原来的任意范围缩小的一个点
*/
public static <T extends Comparator<T>> void genericMethod(T person)
{
System.out.println("泛型测试...");
}
// 这样传入的类型就只能够是 String 类型了
public static <T extends Comparator<String>> void genericMethod(T person)
{
System.out.println("泛型测试...");
}
上界:类型参数不能够使用
super
关键字作为上界进行限制
// 这种声明式错误的
public static <T super Student> void genericMethod(T student)
{
/*
原因分析:
1. 类型擦除导致的
2. 虚拟机执行类型擦除之后会导致 <> 中存放 T 类型而不是 Student 类型
3. 而 T 类型也会被擦除称为 Object 类型
4. 导致无论任何具体类型都可以传入,那么 super 的限制就没有任何实际意义了
*/
}
泛型继承规则
普通泛型继承规则
- 图示
代码:
class Person
{
...
}
class Student extends Person
{
...
}
public static void method(List<Person> people)
{
...
}
public static void main(String[] args)
{
List<People> people = new ArrayList<>();
List<Student> students = new ArrayList<>();
method(people); // 编译通过
method(students); // 编译无法通过
}
通配符泛型继承规则
下界限制继承
上界限制继承
泛型通配符
概念:在我们根本不知道要传入何种具体类型时,将通配符作为==具体类型==传入
表现形式:
<?>
特点:
- 通配符不是类型参数,不能够用于声明泛型类,泛型接口,泛型方法
- 通配符并不是一种具体类型
- 通配符可以使用上界(
super
)和 下界(extends
)
类型
无限定通配符
通配符不可以用于声明变量
public static void main(String[] args)
{
/*
1. 通配符声明是直接报错的 -> 因为通配符不是任何具体的类型,并不等价于 Object
*/
? element = null;
}通配符可以用于局部变量的声明中 -> 并非没有实际意义
注:
- 表示对于这个泛型类我们不关心存储的元素类型,只关心和元素类型无关的方法 -> 诸如获取集合长度,判断集合是否为空…
- 用于方法的形参中可以解除泛型的严格限制
- 提高了代码的可阅读性
/*
1. 正如之前所提到的可以使用通配符作为具体参数类型传入
2. 因为几乎无法调用泛型类中的任何方法
*/
List<?> people = new ArrayList<>();
// 根据上面的声明传入通配符
List<?>
{
// 显然我们不可能声明一个 ? 类型的变量去接收
? getElement();
// 显然根据第一条规则赋值方法是根本不可能使用的
void setElement(? element);
// 但是我们只要类中存在不涉及 ? 类型的方法,仍然是可以调用的
int size()
{
// 这种不需要涉及到使用 ? 类型的方法都是可以调用的
...
}
}通配符用于方法中
/*
1. List<String>,List<Integer> 所有类型的集合都可以作为参数传入
2. 而不是传入某个特定的泛型类
*/
public static void method(List<?> list)
{
...
}
通配符子类型限定
限定通配符声明局部变量
/*
1. 不同于无限定通配符几乎丧失了所有读写的能力
2. 子类型限定通配符仍然具有读取的能力
3. 原因我也不知道
*/
List<? extends Person> people = new ArrayList<>();
people.get();// 编译通过
people.add();// 编译不通过限定通配符用于方法中
/*
1. 限定了作为形参传入的类型 -> List<String>,List<Integer> 都不可以传入,只有 List<Student> 可以传入
2. List<?> 是不可以被捕获的,但是 List 可以
*/
public static void method(List<? extends Person> list)
{
...
}
通配符超类型限定
限定通配符声明局部变量
/*
1. 不同于无限定通配符几乎丧失了所有读写的能力
2. 超类型限定通配符仍然具有存储的能力
3. 但是存储的类型只可以是 Student 类型 -> 其父类都是不可以存储的
*/
List<? super Student> student = new ArrayList<>();
student.add(new Student()); // 编译通过
student.get(); // 编译不通过限定通配符用于方法中
/*
1. 限定了作为形参传入的类型 -> List<String>,List<Integer> 都不可以传入,只有 List<Student> List<Person> List<Object> 可以传入
2. List<?> 是不可以被捕获的,但是 List 可以
*/
public static void method(List<? super Student> list)
{
...
}
注:如果局部变量中使用无限定通配符,那么形参中也必须使用无限定通配符才能够捕获,只要形参有限定都是不可以捕获无限定通配符到的
类型擦除
引入
问题:
List<String> strings = new ArrayList<String>();
List<Integer> integers = new ArrayList<Integer>();
/*
1. 这两个集合类的类型相同吗?因为传入的具体类型不同所以反射得到的类型也不同?还是说是相同的
2. 答案是相同的
*/
System.out.println(strings.getClass() == integers.getClass());历史背景
Java 与 C#
底层层面:
- Java 并没有实现真正的泛型化 -> 采用==类型擦除式泛型==;
- C# 实现了真正的泛型 -> 采用==具现化泛型==
源码层面:
Java 采用的是在原有的集合类中的直接进行泛型化
/*
1. 原有的不带泛型的集合类(ArrayList,List...)可以声明,但是在源码中已经不存在了
2. 只提供了带有泛型的集合类
*/
ArrayList<T> List<T>...C# 采用的是增加带有泛型的集合类,和原有的不带有泛型的集合类进行区分
Java 采取如此策略的原因
底层原因:
- 采用具现化泛型则意味着需要对字节码文件,虚拟机的规范做出修改
- Java 语言规范中保证老版本的源文件在新版本的 JDK 中同样能够正常编译
- 既然字节码文件的格式,以及虚拟机的规范都发生了变化,那么老版本的源文件显然是不可能新版本的 JDK 上编译的
原码原因:
- 既然不用在字节码和虚拟机的层面去实现泛型,那么源码层面的泛型怎么实现呢?
- Java 采用在原有的集合类的基础上直接进行泛型化,而不是新增加泛型类
- Java 在需要增加泛型机制时已经经过了数十年的发展,许多方法中已经使用原有的集合类作为返回值
- 如果此时再添加新的泛型集合类显然会将集合类库变得更加庞大(骂的人更多)
遗留问题:原有的集合类如何新版本的泛型集合类交互呢?-> 让原有的集合类作为原始类成为所有泛型集合类的父类
ArrayList<String> strings = new ArrayList<>();
ArrayList arrayList = new ArrayList();
arrayList = strings; // 编译可以通过
概念:源代码在进入虚拟机之前所有的类型参数都会被==抹去==
规则:
无限定类型参数:
- 所有==类型参数==全部被替换成为
Object
- ==泛型类==会转换成为==原始类==
从最简单的泛型擦除规则可以看出来,泛型在 Java 中仅仅是相当于一种语法糖,底层仍然采用的是多态去实现泛型
/*
进入虚拟机之前的源代码
*/
class Generic<T>
{
T genericFiled;
public T method(T first, T second)
{
...
}
}
/*
进入虚拟机之后的源代码
*/
class Generic
{
Object genericFiled;
public Object method(Object first, Object second)
{
...
}
}
public static void main*(String[] args)
{
//源码层面: 类型参数将会被抹去生成原类型
Generic<String> generic = new Generic<String>();
// 虚拟机层面:
Generic generic = new Generic();
// 虚拟机中:使用带有泛型的变量时是需要进行强制转换的
String str = (String)generci.method("123","456");
}- 所有==类型参数==全部被替换成为
有限定类型参数
限定类
- 所有类型参数都会转换为==限定的类==
- 泛型类会==继承==当前这个限定类
/*
进入虚拟机之前的源代码
*/
class Generic<T extends Person>
{
T genericFiled;
public T method(T first, T second)
{
...
}
}
/*
进入虚拟机之后的源代码
*/
class Generic
{
Person genericFiled;
public Person method(Person first, Person second)
{
...
}
}
public static void main(String[] args)
{
//源码层面: 类型参数将会被抹去生成原类型
Generic<Student> generic = new Generic<Student>();
// 虚拟机层面:
Generic generic = new Generic();
// 虚拟机中:使用带有泛型的变量时是需要进行强制转换的
Person str = (Person)generci.method(new Student(),new Student());
}限定接口
规则
- 所有类型参数都会转换为==限定的接口==
- 泛型类会==实现==当前这个限定接口
问题:如果限定条件中有多个接口?虚拟机如何选择?
解决方式:虚拟机会选择==第一个接口==作为限定的接口
影响:所以限定接口的顺序会导致虚拟机选择的接口不一样,导致强转的类型不一样
/*
进入虚拟机之前的源代码
*/
class Generic<T extends Comparable & Serializable>
{
T genericFiled;
public T method(T first, T second)
{
...
}
}
/*
进入虚拟机之后的源代码
*/
class Generic
{
// 选择第一个限定接口作为具体的类型
Comparable genericFiled;
public Comparable method(Comparable first, Comparable second)
{
...
}
}
类型擦除产生的问题:
重载问题
/*
1. List<String> List<Integer> 根据类型擦除机制 -> 显然这两个参数的类型是一致的都是 List
2. 重载必须保证相同方法名中的参数类型必须不一致,这里的参数类型一致所以显然重载失败
*/
public static void method1(List<String> strings)
{
}
public static void method1(List<Integer> integers)
{
}
//------------------------------------------------------------------
/*
1. 重载必须保证方法签名不同,但是返回类型并不属于方法名中的一部分,所以编译是根本无法通过的
2. 但是在虚拟机中的重载并不是这样的
3. 虚拟机中只要保证 返回类型 + 方法签名 是不同的就可以证明两个方法不同 -> 是可以共存的
*/
public static String method1(List<String> strings)
{
}
public static int method1(List<Integer> integers)
{
}继承问题
class Person<T>
{
T first;
public void setFirst(T first)
{
this.first = first;
}
public T getFirst()
{
return first;
}
}
class Student extends Person<String>
{
/*
1. 子类重写父类的方法所以理所应当调用的应该是子类的的方法
2. 但是由于类型擦除导致父类新增了两个方法
public void setFirst(Object first)
public Object getFirst()
3. 显然这两个新增的方法也会被子类继承,但是这样就达不到重写的效果,虚拟机可能根本不知道到底调用哪个方法
4. 桥方法诞生用于解决这个问题:
虚拟机中采用 方法嵌套调用的方式解决
public void setFirst(Object first)
{
this.setFirst((String) first) 强制转换调用子类的方法
}
*/
public void setFirst(String first)
{
super.first = first;
}
public String getFirst()
{
return super.getFirst();
}
}自动拆装箱问题
- 回顾:前面提到类型参数不能够接收基本数据类型(
int
double
…) - 原因:类型擦除后,显然需要在
int
和Object
之间进行强制转换,这显然是不可以的 -> 只能够允许传入int
的包装类Integer
- 影响:调用方法时会进行很多次的无意义的自动拆装箱,导致 Java 泛型的效率非常低
- 回顾:前面提到类型参数不能够接收基本数据类型(
数组问题:不能够声明泛型数组
/*
以下代码都是不可以运行的
*/
public static void main(String[] args)
{
// 源码层面
ArrayList<String>[] strings = new ArrayList<>[20];
// 虚拟机中
Object[] strings = new Object[20];
// 显然此时任何类型都可以作为元素加入数组中 -> 在编译阶段并不会报错
strings[0] = new Integer(1);
// 但是虚拟机执行这行代码时,会检查存储类型是否正确,最后会抛出存储类型异常
// 有个问题,虚拟机怎么知道原有的存储类型是什么?
}