简介:
泛型是将接口的概念进一步延伸,"泛型"的字面意思是广泛的类型。类、接口、和方法代码可以应用于非常广泛的类型。代码和他们能操作的数据类型不在绑定在一起了,同一套代码可以用于多种数据类型,这样不仅可以复用代码,降低耦合,而且可以提高代码的可读性和安全性。
泛型类
**
* GenericClass 后面的T表示类型参数,泛型就是类型参数。
* 处理的数据类型不是固定的,可以作为参数传入
* @param <T>
*/
static class GenericClass<T>{
T name;
T schoolName;
public GenericClass(T name, T schoolName) {
this.name = name;
this.schoolName = schoolName;
}
}
static class GenericMultipleParameter<T,U> {
T name;
U secore;
public GenericMultipleParameter(T name, U secore) {
this.name = name;
this.secore = secore;
}
}
//泛型类用法,String 是传递的实际类型参数,GenericClass类的代码和它处理的数据类型不是绑定的,具体类型可以变化。
GenericClass<String> genericClass = new GenericClass<>("黄皮皮","黄小小中学");
String name = genericClass.name;
System.out.println(name);
//上面是String类型的数据,下面可以变为int,通过传递类型参数,可以实现代码和数据的分离,提高代码的复用性和可读性
GenericClass<Integer> integerGenericClass = new GenericClass<>(10,20);
int intName = integerGenericClass.name;
System.out.println(intName);
//类型参数可以是多个
GenericMultipleParameter<String,Double> genericMultipleParameter =
new GenericMultipleParameter<>("黄皮皮",20.0d);
System.out.println(genericMultipleParameter.name);
基本原理:
我们知道java有编译器和虚拟机,java编译器将java源码转换为.class文件,而虚拟机加载并运行.class文件。对于泛型类,java编译器会将泛型代码转换为普通的代码。会将类型参数擦除,替换为Object,在插入必要的强制类型转换。JAVA虚拟机实际执行的时候,是不知道泛型这回事的,只知道普通的类和代码。在强调一遍,Java 是通过擦除实现的,类定义的类型参数T会被替换为Object。程序运行时,不知道泛型的实际参数类型。比如上面的GenericClass,运行时只知道GenericClass,而不知道Integer。
那小伙伴的疑问就来了,为啥要用泛型呢?通过下面例子会给你答案:
static class GenericPrinciple {
Object name;
Object secore;
public GenericPrinciple(Object name, Object secore) {
this.name = name;
this.secore = secore;
}
}
GenericPrinciple genericPrinciple = new GenericPrinciple(1,2);
int objName = (int)genericPrinciple.name;
System.out.println(objName);
GenericPrinciple genericPrinciple1 = new GenericPrinciple("黄皮皮",20.0d);
System.out.println(genericPrinciple1.secore);
//既然Object也可以实现,那为啥还要用泛型呢?主要时因为泛型的两大好处:更好的 安全性和更好的可读性。
//再看下面的代码,在编译的时候是没问题的,运行时会报java.lang.ClassCastException异常
GenericPrinciple genericPrinciple2 = new GenericPrinciple("黄皮皮",90.0d);
int name1 = (int)genericPrinciple2.name;
String secore = (String)genericPrinciple2.secore;
System.out.println(name1 + secore);
//如果用泛型就不会有上面的问题,在编译的时候就会提示类型错误,这样就能避免bug在运行时才能被发现。
GenericMultipleParameter<String,Double> genericMultipleParameter1 =
new GenericMultipleParameter<>("黄皮皮",99.0d);
//这里会提示类型错误,
int name2 = genericMultipleParameter1.name;
泛型最常用的应用:容器
下面例子是写了一个简易的动态数组:
static class DynamicArray<E> {
private static final int DEFAULT_CAPARITY = 10;
private int size;
private Object[] elementData;
public DynamicArray() {
this.elementData = new Object[DEFAULT_CAPARITY];
}
public void ensureCapacity(int minCapacity){
int oldCapacity = elementData.length;
if(oldCapacity > minCapacity){
return;
}
int newCapacity = oldCapacity*2;
if(newCapacity < minCapacity){
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData,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 e){
E oldValue = get(index);
elementData[index] = e;
return oldValue;
}
public <T extends E> void addAll(DynamicArray<T> e){
for (int i = 0; i<e.size; i++){
add(e.get(i));
}
}
public void addAllCharacter(DynamicArray<? extends E> e){
for (int i = 0; i<e.size; i++){
add(e.get(i));
}
}
public void copyTo(DynamicArray<? super E> arr){
for (int i = 0; i<size; i++){
arr.add(get(i));
}
}
}
//泛型最常用的就是作为容器类,上面实现一个简易的动态数组 DynamicArray<E>
//它可以使用任意数据类型,且类型安全。传入的类型参数可以是一个泛型类,可以是String
DynamicArray<GenericClass<String>> genericClassDynamicArray = new DynamicArray<>();
genericClassDynamicArray.add(genericClass);
DynamicArray<String> dynamicArray = new DynamicArray<>();
dynamicArray.add("黄皮皮");
泛型方法:
泛型方法与它所在的类是不是泛型类没有关系。同时如果用泛型方法可以解决的问题,就不用泛型类。
public static <T> int indexOf(T[] arr, T pro){
for (T t:arr) {
if(t.equals(pro)){
return (int)t;
}
}
return -1;
}
public static <T,U> void MultipleParameter(T t,U u){
System.out.println(""+t.toString()+u);
}
Integer[] ints= new Integer[]{1,24,2,5,6,8,12,45,124};
int pro = 5;
if(GenericDemo.indexOf(ints,pro) != -1){
System.out.println("数组中存在匹配的值");
}
//泛型方法也是可以多个类型参数,泛型方法可以不用特意指定类型参数的实际类型,java编译器可以自动推断出来
GenericDemo.MultipleParameter("黄皮皮",90.d);
泛型接口:
接口也可以是泛型的,比如Comparable和Comparator 接口。但是实现接口时,应该指定接口泛型的具体的类型。比如下面这个例子:
//实现Comparable<T>泛型接口时需要指定泛型接口具体的类型参数
static class GenericInterface implements Comparable<Integer> {
@Override
public int compareTo(Integer integer) {
return 0;
}
}
类型的参数限定:extends
无论是泛型类,泛型接口、还是泛型接口,关于类型参数,我们都 知之甚少,只能把类型参数当做是Object。但JAVA 支持限定这个参数的一个上界。也就是说,参数必须为给定上界类型或其子类型。这个限定是通过extends关键字来表示的。这个上界可以是某个类,某个接口,可以是其他的类型参数。
示例:
//比如上界是某个类
static class GenericLimit<T extends Number,U extends Number> {
}
/**
* 比如上界是某个接口
*
*/
static class GenericLimintInterface<T extends Comparable<T>> {}
/**
* 比如上界是其他类型参数
*/
public void addAllCharacter(DynamicArray<? extends E> e){
for (int i = 0; i<e.size; i++){
add(e.get(i));
}
}
/**
* 通配符:更简洁的参数类型限定
* 比如 DynamicArray<E> 的方法adlAll(),可以使用通配符的方式变的更简单
* addAll(DynamicArray<? extends E> c),?表示通配符,<? extends E>表示有限定通配符
* 匹配E 或者E 的子类,具体子类是什么类型是未知的。但是通配符这种方式和之前的定义一个类型参数T的效果是一样的
* 但是更简洁。
* /**
* <T extends E> 和 <? extends E> 同样是extends关键字,它们有啥不同呢?它们用的地方不一样:
* 1)<T extends E> 用于定义类型参数,它声明了一个类型参数T,可放在泛型类定义中类名后面,泛型方法返回值前面
* 2)<? extends E> 用于是实际类型参数,它用于实际化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是
* E或E的某个子类型
* 比如:
* public void addAll(DynamicArray<? extends E> c)
* public <T extends E> void addAll(DynamicArray<T> c)
* 这两种写法可以达成同一个目标
*/
public void addAll(DynamicArray<? extends E> c){
for (int i = 0; i<e.size; i++){
add(e.get(i));
}}
public <T extends E> void addAll(DynamicArray<T> e){
for (int i = 0; i<e.size; i++){
add(e.get(i));
}
}
/**
* 理解通配符,以及通配符的限制
* DynamicArray<?>是无限定通配符,上面的DynamicArray 成为有限通配符
*/
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;
}
/**
*上面也可以改为类型参数的方式,不过通配符的方式更加简洁,但上面通配符方法只 能读不能写。
*/
public static <T> int indexOf1(DynamicArray<T> arr,Object elm){
for (int i = 0 ; i<arr.size;i++){
if(arr.get(i).equals(elm)){
return i;
}
}
return -1;
}
/**
*通配符 extends无法写入,因为类型是未知的,所有有时候类型参数方法和通配符 方法结合起来用,可以达到很不错的效果
* 如下:两个元素交换位置
*/
public static void swap(DynamicArray<?>arr, int i, int j){
swapInternal(arr,i,j);
}
public static <T> void swapInternal(DynamicArray<T> arr,int i,int j){
T temp = arr.get(i);
arr.set(i,arr.get(j));
arr.set(j,temp);
}
/**
* 如果参数类型之间有依赖关系,只能使用参数类型
*比如:D和S 有依赖关系,要么相同,要么是D的子类,否则类型不匹配,有编译错误。
*/
public static <D,S extends D> void copy(DynamicArray<D> dest,DynamicArray<S> src){
for (int i = 0; i<src.size();i++){
dest.add(src.get(i));
}
}
/**
*超类型通配符,与形式<? extends T>相反,它的形式为<? super T> 称为超类 型通配符,表示T的某个父类型,
* 有了它我们就可以更灵活的写入了
* 比如给DynamicArray 添加一个copyTo方法
*/
public void copyTo(DynamicArray<? super E> arr){
for (int i = 0; i<size; i++){
arr.add(get(i));
}
}
//通配符的限制:下面例子通配符只能读不能写。看例子:
DynamicArray<Integer> integerDynamicArray = new DynamicArray<>();
DynamicArray<? extends Number> dynamicArray1 = integerDynamicArray;
Integer i = 1;
//下面是那种三种添加方式都会编译出错,为啥?因为通配符表示类型安全不知,? extends Number表示是Number的
//某个子类型,但不知道具体类型。如果允许写入,Java 将无法保证类型安全性。所以直接禁止了。
/* dynamicArray1.add(i);
dynamicArray1.add((Number)i);
dynamicArray1.add((Object)i);*/
//通过<? super E>就可以灵活的写入了
DynamicArray<? super Number> dynamicArray2 = new DynamicArray<>();
dynamicArray2.add(1);
dynamicArray2.add(29.0d);
泛型的细节和局限性:
//泛型的细节和局限性
//(1)基本类型不能用于实例化类型参数。如下: 只能转为使用基本类型的包装类
/* GenericTest<int> genericTest = new GenericTest<int>(1);*/
//(2)运行时类型信息不使用泛型。比如直接类名.class,因为类型对象只有一份,和泛型无关
/* Class<?> cls = GenericTest<Integer>.class;*/
//下面两个用getClass()返回的泛型类Class对象是相同的
GenericTest<Integer> intG = new GenericTest<>(1);
GenericTest<String> stringG = new GenericTest<String>("2");
if(intG.getClass().equals(stringG.getClass())){
System.out.println("This is true");
}
//(3)类型擦除引发的冲突,下面编译就会报错
if(intG instanceof GenericTest<Integer>){
System.out.println("类型擦除引发的冲突");
}
//但支持下面这种写法
if(intG instanceof GenericTest<?>){
System.out.println("允许通配符的方式");
}
//(4)不能通过类型参数创建对象
T t = new T();
//(5)泛型类类型参数不能用于静态变量和方法
//(6)Java 还支持多个上届,多个上届之间以&分割 T extends Base &Comparabl & Serializable
总结:
泛型使我们在编写代码时可以更加的灵活,更加的易读,同时提高了代码的安全性。使我们可以在编译的时候就可以找出代码漏洞,减轻后期系统问题。