1. 范型的定义和使用
class MyStack<T>{
//编译的时候 T 被擦除为 Object
private T[] e;
private int usedSize;
MyStack() {
this.e = (T[])new Object[5];
}
void push(T val){
this.e[this.usedSize++] = val;
}
T pop(){
return this.e[this.usedSize-1];
}
}
public class main {
public static void main(String[] args) {
//编译的时候 Integer 被擦除为 Object
MyStack<Integer> stack = new MyStack<>();
stack.push(1);
System.out.println(stack.pop());
}
}
输出:
1
T 只是一个占位符
泛型的意义: 1⃣️自动进行类型检查, 2⃣️自动进行类型转换
范型是怎么编译的?
泛型是编译期间的一种机制【擦除机制】: 在编译的时候会讲 T 擦除为 Object 类型
stack.push("Hello");// 编译会报错
有同学可能会想, 既然 Integer 会被擦除为 Object 类型, 为何不能赋值一个 String 对象呢?
其实这就是泛型的意义1⃣️的作用, 它会拿着 Integer 来进行对传入的参数进行检测, 提前告知我们程序错误.
也有一种称为 裸类型 的泛型
在定义的时候不穿入任何包装类:MyStack stack = new MyStack();
class MyStack<T>{
private T[] e;
private int usedSize;
MyStack() {
this.e = (T[])new Object[5];
}
void push(T val){
this.e[this.usedSize++] = val;
}
T pop(){
return this.e[--this.usedSize];
}
}
public class main {
public static void main(String[] args) {
MyStack stack = new MyStack();
stack.push(1);
stack.push("Hello");
System.out.println(stack.pop());
System.out.println(stack.pop());
}
}
输出:
Hello
1
由于此类泛型意义不大, 最重要的是缺少了类型检测, 后续可能会引发未知的错误因此最好不要使用. 使用泛型的作用是使得程序在编译期可以检查出与类型相关的错误,但是如果使用了泛型数组,这种能力就会受到破坏【这也是无法直接创建泛型数组的原因】
T[] = new T[];// 代码会报错, 不可以直接创建泛型数组而是通过强转Object数组实现的创建泛型数组
泛型参数可以有多个而不一定是一个, 还可以继承父类
伪代码
class 泛型类名称<类型形参列表> { // 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数*/
{
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
常见英文字母 | 含义 |
---|---|
E | Element |
K | Key |
V | Value |
N | Number |
T | Type |
2. 泛型方法
就是定义在类内部的static修饰的函数【类的静态方法】
实现一个类, 计算最大值
class Algorithm {
static <T extends Comparable> T findMax(T[] arr) {// static里边定义泛型边界.
T max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (max.compareTo(arr[i]) < 0) {// 引用类型不是简单类型, 所以不能使用运算符来比较大小, 因此必须借用 Comparable 接口
max = arr[i];
}
}
return max;
}
}
public class Demo07范型 {
public static void main(String[] args) {
Integer[] arr = {19, 91, 28, 82};
Integer ret = Algorithm.findMax(arr);
System.out.println(ret);
String[] arr2 = {"AB", "CD", "EF"};
String ret2 = Algorithm.findMax(arr2);
System.out.println(ret2);
}
}
91
EF
这里在进行类型擦除的时候擦除为 Comparable
3. 范型类的边界
为了了解边界的使用, 先看看这个问题【和泛型方法一样】
class Algorithm<T extends Comparable> {
T findMax(T[] arr) {
T max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (max.compareTo(arr[i])<0) {
max = arr[i];
}
}
return max;
}
}
public class main {
public static void main(String[] args) {
Integer[] arr = {19,91,28,82};
Integer ret = new Algorithm<Integer>().findMax(arr);
System.out.println(ret);
String[] arr2 = {"AB", "CD", "EF"};
String ret2 = new Algorithm<String>().findMax(arr2);
System.out.println(ret2);
}
}
输出
91
EF
3.1 通配符
class GenericPrint {
static <T> void print(ArrayList<T> arrayList) {
for (T obj : arrayList) {
System.out.print(obj + " ");
}
System.out.println();
}
}
public class main {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
GenericPrint.print(list);
ArrayList<String> list1 = new ArrayList<>();
list1.add("Hello");
list1.add("World");
GenericPrint.print(list1);
}
}
输出
1 2 3 4
Hello World
这里的 T 指定了是 Integer, String. 传入
ArrayList<T> arrayList
时候就知道打印的数据类型. 如果不指定类型会怎样呢?
class GenericPrint {
static <T> void print(ArrayList<?> arrayList) {
for (Object obj : arrayList) {
System.out.print(obj + " ");
}
System.out.println();
}
}
public class main {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
GenericPrint.print(list);
ArrayList<String> list1 = new ArrayList<>();
list1.add("Hello");
list1.add("World");
GenericPrint.print(list1);
}
}
输出
1 2 3 4
Hello World
这里给的是一个
Object
的类型, 所以可以接受任何类型的数据进行打印输出
那对于形参就没有区别吗?
我们看看 add, get 方法看看编译器的形参类型如何提示的
我们发现对于
get
方法是没有区别的 , 都是选取的 index 下标进行返回, 只不过对于有确定类型的就会返回确定的数据类型 T
add
方法 多了一个 caputre of ? e 这样的提示, 并没有一个确定的数据类型, 而指定泛型参数后就会有一个确定的形参类型
因此对于制定类型的泛型更适合写入数据也就是add
操作【写入数据更明确】, 对于通配符的泛型更适合读取数据也就是get
操作【读取数据不受规定泛型检查限制更方便】
因此对于 print
函数而言, 由于使用了通配符, 所以以下传参都是正确的
static <T> void print(ArrayList<?> arrayList)
print(new ArrayList<Integer>());
print(new ArrayList<Number>());
print(new ArrayList<String>());
print(new ArrayList<Object>());
...
3.2 通配符-上界
通配符的上下界规定了参数的"最大值"【不能超过上界的范围】
// 传入的参数必须是 Number 的子类型
static <T> void print(ArrayList<? extends Number> arrayList)
// 以下传参正确
print(new ArrayList<Integer>());
print(new ArrayList<Number>());
//以下传参会被泛型检查异常
print(new ArrayList<String>());
print(new ArrayList<Object>());
3.3 通配符-下界
通配符的上下界规定了参数的"最小值"【不能低于下界的范围】
// 传入的参数必须是 Integer 的父类型
static <T> void print(ArrayList<? super Integer> arrayList)
// 以下传参正确
print(new ArrayList<Integer>());
print(new ArrayList<Number>());
print(new ArrayList<Object>());
//以下传参会被泛型检查异常
print(new ArrayList<String>());
print(new ArrayList<Double>());
4. 泛型的父子类型
class MyarrayList<T>{
}
public class main {
public static void main(String[] args) {
MyarrayList<Number> myarrayList = new MyarrayList<Integer>();// 虽然 Number 和 Integer是父子类关系, 但对于容器 myarrayList 而言它们之间不构成父子类关系
MyarrayList<Object> myarrayList1 = new MyarrayList<Integer>();
MyarrayList<? extends Number> myarrayList2 = new MyarrayList<Integer>();// 使用通配符来确定父子类关系
MyarrayList<? extends Object> myarrayList3 = new MyarrayList<Integer>();
}
}
5. 泛型限制
- 泛型类型参数不支持基本数据类型
- 无法实例化泛型类型的对象
- 无法使用泛型类型声明静态的属性
- 无法使用 instanceof 判断带类型参数的泛型类型
- 无法创建泛型类数组
- 无法 create、catch、throw 一个泛型类异常(异常不支持泛型)
- 泛型类型不是形参一部分,无法重载
小提示
区别点 | 重载 | 重写 |
---|---|---|
参数列表 | 必须修改 | 一定不能修改 |
返回值要求 | 可以修改 | 一定不能修改 |
异常 | 可以修改 | 可以减少或删除,一定不能抛出新的或者更广的异常 |
访问 | 可以修改 | 一定不能做更严格的限制(可以降低限制) |
总结
方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
- 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。外壳与核心都改变
- 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。外壳不变, 内部核心改变
- 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
class MyarrayList<T> {
//无法创建泛型静态变量
static T t;
//无法直接创建泛型数组
T[] arr = new T[5];
T[] arr1 = (T[]) new Object[5];
// 当参数个数不同的时候可以重载
private T func(T args) {
return args;
}
private void func(T args1, T args2) {
}
// 当参数个数相同的时候无法重载
private T func1(T args) {
return args;
}
private void func1(T args1) {
}
}
public class main {
public static void main(String[] args) {
MyarrayList<Integer> arr1 = new MyarrayList<>();// 泛型类型参数必须要是包装类
try {// 泛型不支持类的异常捕获
MyarrayList<int> arr;//泛型类型参数不支持基本数据类型
} catch (Exception e) {
e.printStackTrace();
}
// 无法用 instaceof 判断泛型类型
main test = new main();
if (test instanceof main){}
if (arr1 instanceof Integer){}
}
}
这次文章篇幅较少, 旨在了解泛型和使用规范, 主要目的是理解泛型能够查看源码【以优先级队列位列/小堆】
PriorityQueue
是一个泛型, 继承了抽象队列AbstractQueue
实现了 序列化java.io.Serializable
接口
Comparator
形参是 泛型 E 的下边界, 传入的参数范围不能低于 E
Java泛型, 是不是很简单? 其实在实际应用中还有7 条建议【摘自 EffectiveJava中第五章-泛型】
- 请不要再新代码中使用原生代码
- 消除非受察警告
- 列表优先于数组
- 优先考虑泛型
- 优先考虑泛型方法
- 利用限制通配符提升 API 灵活性【对于编程而言, 灵活也可能是贬义词】
- 优先考虑类型安全的异构容器