目录
一、泛型的概念
(一)什么是泛型
在类和方法的使用中,一般只能使用具体的类型,基本类型亦或是自定义的类。但想要其适用于多种类型的代码编写。因此产生了泛型来处理这种刻板的限制对代码的束缚。简单的来讲,泛型就是适用于很多类型。从代码上讲,就是对类型实现了参数化。
(二)为何引入泛型
想要在数据结构集合中适用于多种数据类型,但又不是所有的类型都能拿来存储,而是选取自己想要的数据类型进行存储,因此引入泛型,便能很好地处理这种情况。同时泛型的引入,能有效的检查编译中存在的错误数据,更好的体现了java语言的安全性。
public class MyArrayList {
private final int capacity = 10;
private int usedSize;
//Object[]数组能存储任何类型的元素
private Object[] elem;
public MyArrayList() {
elem = new Object[capacity];
}
//增添元素
public void add(Object val) {
if(usedSize != capacity) {
elem[usedSize++] = val;
} else {
System.out.println("容量溢出");
}
}
//获取元素
public Object get(int pos) {
return elem[pos];
}
public static void main(String[] args) {
//在自定义的集合中能够存储任何类型的元素,显然不是程序员想要的结果
MyArrayList myArrayList = new MyArrayList();
myArrayList.add("a");//字符串型
myArrayList.add(1);//整型
myArrayList.add(2.0);//double型
//每次读取元素的时候都要强制类型转换
String s = (String) myArrayList.get(0);
Integer a = (Integer) myArrayList.get(1);
}
}
(三)泛型在集合中的使用
语法:
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
为使上述问题得到解决,使用泛型对该自定义集合类进行改进。
public class MyArrayList1<T> {
private final int capacity = 10;
private int usedSize;
private T[] elem;
public MyArrayList1() {
//报错,不能创建一个泛型数组
//elem = new T[capacity];
//不报错,但是存在问题,此问题留到下文讲擦除机制时讲
elem = (T[]) new Object[capacity];
}
//真正正确的创建泛型数组的方式--运用反射
public MyArrayList1(Class<T> clazz,int capacity) {
elem = (T[]) Array.newInstance(clazz,capacity);
}
//增添数据
public void add(T val) {
if(usedSize != capacity) {
elem[usedSize++] = val;
} else {
System.out.println("容量溢出");
}
}
//读取数据
public T get(int pos) {
return elem[pos];
}
public static void main(String[] args) {
//使用泛型后自动进行编译检查和类型转换
MyArrayList1<String> myArrayList = new MyArrayList1<String>();
myArrayList.add("a");
//报错
//myArrayList.add(1);
//myArrayList.add(2.0);
//读取元素时不需要强制类型转换
String s = myArrayList.get(0);
}
}
注意:
泛型实现数据类型参数化,传入的数据类型必须是基本数据类型的包装类。
二、泛型的使用(以下用代码进行讲述)
(一)泛型类
语法:
泛型类 < 类型实参 > 变量名 ; // 定义一个泛型类引用new 泛型类 < 类型实参 > ( 构造方法实参 ); // 实例化一个泛型类对象
代码:
//指定一个泛型类引用泛型类的对象 ArrayList<String> list = new ArrayList<String>();//后面的String可以省略 //自定义一个交换数据类 class Swap<T> {} public static void main(String[] args) { Swap<String> swap = new Swap<>(); }
(二)泛型方法
(1)普通泛型方法
语法:
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
代码:
//定义交换两个数据值的类
class Swap<T> {
//普通泛型方法
//(形式一)
public void swap(T[] array, int i, int j) {
T tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
//(形式二)
public <T> void swap(T[] array, int i, int j) {
T tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
(2)静态泛型方法
语法:
方法限定符 static <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
代码:
//泛型的静态方法
class Swap {
public static<T> void swap(T[] array, int i, int j) {
T tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
注:泛型静态方法不依赖于对象,所以不在class类后面添加泛型
(三)泛型接口
语法:
interface 泛型接口名<类型形参列表> {....}
代码:
//在此用Comparable接口来举例
//此接口适用于任何类型的比较
interface Comparable<T> {
public int compareTo(T o);
}
(四)泛型上界及其擦除机制
(1)泛型上界
在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
语法:
class 泛型类名称<类型形参 extends 类型边界> { ... }
代码:
//泛型上界为Number
class MyArray<E extends Number> {
public static void main(String[] args) {
//创建对象时只能是Number或Number的子类
MyArray<Integer> myArray1 = new MyArray<Integer>();
MyArray<Double> myArray2 = new MyArray<Double>();
//报错,String不是Number的子类
MyArray<String> myArray = new MyArray<String>();
}
}
特殊实例:
//自定义一个比较类
//泛型方法
class Alg1<T extends Comparable<T>> {
public T findMax(T[] array) {
T max = array[0];
for (T elem: array) {
if(elem.compareTo(max) > 0)
max = elem;
}
return max;
}
}
注意:
该代码块的上界是Comparable接口,当构造该类对象时,必须是实现了Comparable接口,基本数据类型的包装类都是实现了Comparable接口。
(2)java泛型擦除机制
擦除机制是Java5用来实现泛型的技术。一般来说,在运行时阶段,Java编译器先执行类型检查,然后执行擦除或删除泛型信息。而具体化的泛型,与此正好相反。基于具体化泛型系统的类实现在运行时作为顶级实体,它在运行时保留了类型参数,而这给基于类型和反射性的语言提供了确定的操作。
通过查看编译生成的字节码文件,更好的理解java泛型擦除机制
(a)未定义上界类型,最终擦除到Object
源代码:
生成的字节码文件:
(b)定义上界类型为Number,最终擦除到Number
源代码:
生成的字节码文件:
注意:
不能创建泛型数组,会存在隐患,以下用代码进行演示。
class MyArray<T> {
private final int capacity = 10;
private int usedSize;
private T[] elem;
public MyArrayList1() {
//报错,不能创建一个泛型数组
//elem = new T[capacity];
//不报错,但是存在问题,编译的时候,替换为Object[]
elem = (T[]) new Object[capacity];
}
//真正正确的创建泛型数组的方式--运用反射
public MyArrayList1(Class<T> clazz,int capacity) {
elem = (T[]) Array.newInstance(clazz,capacity);
}
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos,T val) {
this.array[pos] = val;
}
//编译时返回的类型是Object[]
public T[] getArray() {
return array;
}
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<>();
//使用getArray()方法,返回的是bject[]类型的数组,不是不能直接用Integer[]来接受的
//数组和泛型的区别是数组是在运行时检查错误,而泛型是在编译时检查错误,为此该语句段未报错
Integer[] strings = myArray1.getArray();
}
}
运行结果:
问题原因:
数组和泛型之间的一个重要区别是它们如何强制执行类型检查。具体来说,数组在运行时存储和检查类型信息,泛型在编译时检查类型错误。
返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Double等等类型,运行的时候,直接转给Integer类型的数组,编译器认为是不安全的。
另外,即使对返回的数组进行强制类型转换为(Integer[])也不能改变其内部元素是其他的数据类型,运行时程序任然会报错。
正确的处理方法是运用反射来创建数组
public class MyArrayList1<T> {
private final int capacity = 10;
private int usedSize;
private T[] elem;
//运用反射创建泛型数组
public MyArrayList1(Class<T> clazz,int capacity) {
elem = (T[]) Array.newInstance(clazz,capacity);
}
}
三、通配符
通配符是用来解决泛型无法协变的问题的,协变指的就是如果 Cat 是Animal 的子类,那么 List<Cat> 也应 该是 List<Animal> 的子类。但是经过泛型的擦除机制,最终两个都擦除到object,所以泛型是不支持这样的父子类关系的。
? 用于在泛型的使用,即为通配符。
(一)通配符的上界
语法:
<? extends 上界 ><? extends Animal > // 可以传入的实参类型是Animal或者Animal的子类
①搞清父子类关系:
<? extends Animal> 是 <Animal>及<Animal子类>的父类
<?> 是 <? extends Animal>的父类
//构成父子类关系
class Animal {
}
class Dog extends Animal {
}
class Dubianquan extends Dog {
}
②父类引用子类对象:
public class Zoon {
public static void main(String[] args) {
//<? extends Animalr> 是 <Animal>及<Animal子类>的父类
ArrayList<? extends Animal> arrayList1 = new ArrayList<Animal>();
ArrayList<? extends Animal> arrayList2 = new ArrayList<Dog>();
ArrayList<? extends Animal> arrayList3 = new ArrayList<Dubianquan>();
//<?> 是 <? extends Animalr>的父类
ArrayList<?> arrayList = arrayList1;
}
}
③注意:
通配符的上界只适合于读取数据,不适用于写入数据。因为通配符上界引用的是<Animal及其子类>的对象,但是不能确定到底是哪个子类,不能存储确定的子类类型,java在编译时会自动查错。
class Animal {
public String toString() {
return "Animal :>";
}
}
class Cat extends Animal {
@Override
public String toString() {
return "Cat :>";
}
}
class Dog extends Animal {
@Override
public String toString() {
return "Dog :>";
}
}
public class Zoon {
//通配符的上界是不适用于写入的,只适合于读取
public static void main0(String[] args) {
ArrayList<? extends Animal> arrayList = new ArrayList<>();
//报错
//arrayList.add(new Animal());
//arrayList.add(new Dog());
//但是可以读取数据,用泛型的上界来读取子类的数据,属于向上转型
Animal animal = arrayList.get(0);
//也可以用Object,因为他是所有类的父类
Object o = arrayList.get(0);
}
}
(二)通配符的下界
语法:
<? super 下界 ><? super Animal > // 代表 可以传入的实参的类型是 Animal 或者 Animal 的父类类型
①搞清父子类关系:
<? super Animal> 是 <Animal>及<Animal父类>的父类
<?> 是 <? super Animal>的父类
//构成父子类关系
class Animal {
}
class Dog extends Animal {
}
class Dubianquan extends Dog {
}
②父类引用子类对象:
public class Zoon {
public static void main(String[] args) {
//<? super Dog> 下界是Dog,引用的是<Dog及其父类>的对象
//因为<? super Dog>的上界没有限制,所以一直可以到Object。
//因此<? super Dog>是<任一Dog及其父类>的父类
//符合父类引用子类对象的规则
ArrayList<? super Dog> arrayList1 = new ArrayList<Animal>();
ArrayList<? super Dog> arrayList2 = new ArrayList<Dog>();
//报错,下界是Dog,不能引用下界以下的子类
// ArrayList<? super Dog> arrayList3 = new ArrayList<Dubianquan>();
//<?> 是 <? super Dog> 的父类
ArrayList<?> arrayList = arrayList1;
}
}
③注意:
通配符的下界只适合于写入数据,不适用于读取数据。因为通配符下界引用的是<Dog及其父类>的对象,但是不能确定读取到的是哪个父类类型,所以引用的类型不能确定,也就不能读取,但是Object是所有类的父类,非要读取的话可以用Object来接受。因为下界存储的内容都是下界以下的数据类型,所以适合写入数据。
class Animal {
@Override
public String toString() {
return "Animal{}";
}
}
class Dog extends Animal {
@Override
public String toString() {
return "Dog{}";
}
}
class Dubianquan extends Dog {
@Override
public String toString() {
return "Dubianquan{}";
}
}
public class Zoon {
public static void main(String[] args) {
ArrayList<? super Dog> arrayList1 = new ArrayList<Animal>();
ArrayList<? super Dog> arrayList2 = new ArrayList<Dog>();
//报错,存储的数据是Dog及其子类的数据类型
// arrayList2.add(new Animal());
//添加的元素 是Dog或者Dog的子类
arrayList2.add(new Dog());
arrayList2.add(new Dubianquan());
//ArrayList<? super Dog> arrayList2引用的是Dog及其父类对象
//编译器会考虑到ArrayList<? super Dog> arrayList2引用的对象如果是 new ArrayList<Animal>();
//那么它存储的数据可能会有Animal类型的数据,那么就不能用Dog来接受Animal类型的数据。
// Dog dog = arrayList2.get(0);//报错
// Animal animal = arrayList2.get(0);//报错 原理同上
//但是Object是所有类的父类,可以使用其来读取数据
Object o = arrayList2.get(0);
}
}
④拓展(运用通配符上下界存储并读取数据,实现多态)
class Animal {
@Override
public String toString() {
return "Animal{}";
}
}
class Dog extends Animal {
@Override
public String toString() {
return "Dog{}";
}
}
class Dubianquan extends Dog {
@Override
public String toString() {
return "Dubianquan{}";
}
}
public class Zoon {
public static void main(String[] args) {
//通配符下界存储数据
ArrayList<? super Dog> arrayList1 = new ArrayList<Dog>();
arrayList1.add(new Dog());
arrayList1.add(new Dubianquan());
//要用通配符下界读取数据,就要用Object来接受
for (Object O: arrayList1) {
System.out.println(O);
}
//利用通配符上界来读取数据
ArrayList<? extends Animal> arrayList = (ArrayList<? extends Animal>) arrayList1;
//因为上述存储的数据类型都是Animal的子类,所以可以用Animal来接受
for (Animal a: arrayList) {
System.out.println(a);
}
}
}