java泛型可以使用三种通配符进行限制分别是什么,类型通配符和泛型方法

  java泛型可以使用三种通配符进行限制分别是什么,类型通配符和泛型方法

  

目录

前面的话1。泛型1.1泛型的用法1.1.1泛型的概念1.1.2泛型1.1.3类型派生1.2裸类型1.3擦除机制1.3.1关于泛型数组1.3.2泛型的编译和擦除1.4泛型的上界1.4.1泛型的上界1.4.2特殊泛型

 

  00-1010本文向您介绍Java语法——泛型和通配符。泛型和通配符是非常抽象的概念。简单来说,两者都可以将类型作为“参数”传递。但是,当您知道传入的是什么类型时,就使用泛型,而当您不确定传入的是什么类型时,就使用通配符。本文将介绍泛型和通配符的使用以及它们之间的区别。

  题外话:泛型和通配符是Java语法中两个比较难的语法。学习泛型和通配符的主要目的是理解源代码,但实际中并没有用到。

  

前面的话

 

  

1.泛型

 

  00-1010 《Java编程思想》上有句话:一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

  所以通用机制是从Java5开始引入的。这个通用是什么意思?由于一般的类和方法只能使用一个特定的类型,所以代码受到很大的约束,比如一个求三个数最大值的方法。假设方法中参数列表的类型一开始是整数,你可以从三个整数数据中找到一个最大值,没有任何问题。这个程序可以完美运行,但是当你想求三个浮点数的最大值时,这个程序就编译不出来了。这时候可以选择基于Double再写一个重载方法实现参数表和实现函数,也能解决问题。但是,你想过一个问题吗?如果有10000个甚至100万个类型需要三个对象中最大的一个,你该怎么办?写100万个重载方法?这是不可能的。为了解决这种类型的问题,引入了泛型。泛型实现了参数化类型的概念,使代码可以应用多种类型,通俗说泛型就是“适用于许多许多类型”的意思。使用泛型将类型作为“参数”传递给类、接口和方法,这样类和方法就可以拥有最广泛的表达能力,不需要因为一个不同的参数而构建另一个类型。

  注意:任意的基本类型都不能作为类型参数。

  

1.1泛型的用法

我们通过一段代码了解泛型。让我们先看看下面这段不使用代码的泛型代码:

 

  /* * *不要使用泛型*/class A { } class Print { private A A;公共打印(A A){ setA(A);system . out . println(this . a);} public void setA(A A){ this . A=A;} public A getA(){ return this . A;}}public class泛型{ public static void main(String[]args){ Print Print=new Print(new A());}}/}}//output:A@1b6d3586不使用泛型访问创建类是没有问题的,但是这个类的复用性不是那么好。它只能保存A类的对象,不能保存任何其他类的对象。我们不想为我们遇到的每个类型都写一个新的类,这是不现实的。当我们学习类的时候,我们知道Object类是所有类的父类,所以Object类可以接受所有的类型引用,我们可以让Print类保存Object类型的对象。

  /* * *使用Object class */class b { } class print 1 { privateobjectb;public Print1(对象b){ setB(b);system . out . println(this . b);} public void print(Object b){ setB(b);system . out . println(this . b);} public void setB(Object b){ this . b=b;} }公共类generic 1 { public static void main(String[]args){ print 1 print 1

   = new Print1(new B());//打印B类型 int i = 2022; print1.print(i);//打印整型类型 print1.print("这是一个字符串对象!");//打印字符串类型 }}//output://B@1b6d3586//2022//这是一个字符串对象!Print1可以接收并打印任何类型,但是这并不是我们想要的结果,你想想如果实现的是一个顺序表类,里面是通过一个数组来实现,如果这个数组什么类型都可以接收,那就非常混乱了,取出数据的时候不能确定取出的到底是什么类型的数据,而且取出的数据是Object类,需要进行强制类型转换,那能不能实现指定类持有什么类型的对象并且编译器能够检查类型的正确性。

  泛型就完美实现了这个目的,下面我们将上述代码改写成泛型类,那么首先得知道泛型的语法,泛型类创建语法如下:

  

class 类名<泛型参数列表> { 权限修饰 泛型参数 变量名;//泛型成员变量 权限修饰 返回值类型 方法名 (参数列表){}//参数列表和返回值类型可以是泛型}

 

  

例如:

 

  

class Print2<T> { private T c; public void print(T c) { setC(c); System.out.println(this.c); } public void setC(T c) { this.c = c; }}

泛型类的使用语法如下:

 

  

泛型类<类型实参> 变量名; // 定义一个泛型类引用new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象

 

  

例如:

 

  

Print2<Integer> print3 = new Print2<Integer>();

使用泛型实现一个类,并使用它:

 

  

/** * 使用泛型 */class C{ }class Print2<T> { private T c; public void print(T c) { setC(c); System.out.println(this.c); } public void setC(T c) { this.c = c; }}public class Generic2{ public static void main(String[] args) { Print2<C> print2 = new Print2<>();//打印C类型 print2.print(new C()); Print2<Integer> print3 = new Print2<>();//打印整型类型 print3.print(2022); Print2<String> print4 = new Print2<>();//打印字符串类型 print4.print("这是一个字符串对象!"); }}/** * output: *C@1b6d3586 * 2022 * 这是一个字符串对象! */

类名后的 <T>代表占位符,表示当前类是一个泛型类。

 

  【规范】类型形参一般使用一个大写字母表示,常用的名称有:

  E 表示 Element

  K 表示 Key

  V 表示 Value

  N 表示 Number

  T 表示 Type

  S, U, V 等等 - 第二、第三、第四个类型

  

//一个泛型类class ClassName<T1, T2, ..., Tn> { }

使用泛型类时,指定了这个类的对象持有的类型,则该对象只能接收该类型的对象,传入其他类型对象,编译器会报错,并且接收泛型类中泛型方法的返回值时,不需要进行强制类型转换(向下转型),而使用Object类需要强制类型转换。

 

  

 

  

1.1.3类型推导

使用泛型类时,可以通过泛型类型中传入的类型来推导实例化该泛型类时所需的类型参数,换个说法,定义泛型对象时,前面的尖括号内必须指定类型,后面实例化时可以不指定。

 

  如:

  

Print2<Integer> print3 = new Print2<>();//后面尖括号内可省略

 

  

1.2裸类型

裸类型其实很好理解,就是一个泛型类,你不去指定泛型对象持有的类型,这样的一个类型就是裸类型。

 

  比如:

  

 public static void main(String[] args) { Print2 print2 = new Print2(); print2.print(2022); print2.print("字符串"); } //output: //2022//字符串

我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制。

 

  

 

  

1.3擦除机制

 

  

1.3.1关于泛型数组

介绍泛型的擦除机制之前,我们先来了解泛型数组·,先说结论,在Java中不允许实例化泛型数组,如果一定要建立一个泛型数组,正确的做法只能通过反射来实现,当然有一个捷径可以不使用反射来创建泛型数组。创建的代码如下:

 

  1.通过捷径创建,大部分情况下不会出错。

  

public class MyArrayList<T> { public T[] elem ; private int usedSize; public MyArrayList(int capacity) { this.elem = (T[])new Object[capacity]; }}

2.通过反射创建,现在只给代码,具体为什么要这么做后续介绍反射再说。

 

  

public class MyArrayList<T> { public T[] elem ; private int usedSize; public MyArrayList(Class<T> clazz, int capacity) { this.elem = (T[]) Array.newInstance(clazz, capacity); }}

 

  

1.3.2泛型的编译与擦除

我们先来实现一个简单的泛型顺序表,不考虑扩容问题,只实现简单的增删操作,来看看构造方法部分编译后的反汇编。

 

  

import java.lang.reflect.Array;public class MyArrayList<T> { public T[] elem ; private int usedSize; public MyArrayList(int capacity) { this.elem = (T[])new Object[capacity]; } public MyArrayList(Class<T> clazz, int capacity) { this.elem = (T[]) Array.newInstance(clazz, capacity); }}

 

  我们发现所有的泛型占位符T都被擦除替换成Object了,这就说明Java的泛型机制是在编译期实现的,而泛型机制实现就是通过像这样的擦除机制实现的,并在编译期间完成类型的检查。

  我们通过打印持有不同类型的MyArrayList类来看看,泛型机制到底是不是不会出现在运行期间,如果是的话,打印出的类型都应该是MyArrayList。

  

 public static void main(String[] args) { MyArrayList<Integer> list1 = new MyArrayList<>(10); MyArrayList<String> list2 = new MyArrayList<>(10); System.out.println(list1); System.out.println(list2); }
 /** * output: * MyArrayList@1b6d3586 * MyArrayList@4554617c */

我们发现打印的类型是一样的,都是MyArrayList,所以可以得出一个结论,泛型是发生在编译期,泛型的类型检查是在编译期完成的,泛型的实现是通过擦除机制实现的,类后面的占位符都会被擦除,其他的占位符都会被替换成Object。当然,这是在泛型参数没有指定上界的情况下,如果存在上界,那占位符会擦除成上界的类型或接口,其实没有指定上界,上界默认为Object,什么是泛型上界,嘘,等一下再说。

 

  根据擦除机制,也能解释为什么Java当中不能实例化泛型数组了,因为泛型数组前面的占位符会被擦除成Object,实际上是创建一个Object数组,而Object数组中什么类型都能放,这就导致取数据时不安全,因为你不能确定数组里面存放的元素全部都是你预期的类型,所以为了安全,Java不允许实例化泛型数组。

  

 

  

1.4泛型的上界

 

  

1.4.1泛型的上界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

 

  

class 泛型类名称<类型形参 extends 类型边界> { ...}

 

  

例如:Number是Integer,Float,Double等相关数字类型的父类。

 

  

public class MyArrayList<T extends Number> {}

那么这个MyArrayList泛型类只能指定持有Number类以及Number的子类,像这样就给泛型的类型传参做了约束,这个约束就是泛型的上界,泛型类被类型边界约束时,只能指定泛型类持有类型边界这个类及其子类。

 

  

 MyArrayList<Integer> list1 = new MyArrayList<>(10);//正确 MyArrayList<Double> list2 = new MyArrayList<>(10);//正确 MyArrayList<String> list3 = new MyArrayList<>(10);//错误,因为String不是Number的子类

 

  

1.4.2特殊的泛型上界

假设需要设计一个泛型类,能够找出数组中最大的元素。

 

  

class MaxVal<T extends Comparable<T>> { public T max(T[] data) { T max = data[0]; for (int i = 0; i < data.length; i++) { if (max.compareTo(data[i]) < 0) max = data[i]; } return max; }}

由于引用类型的比较需要使用Comparable接口来判断大小,所以所传入的类需要实现Comparable接口,上面这个泛型的类型参数的上界是一个特殊的上界,表示所传入的类型必须实现Comparable接口,不过实现了Comparable接口的类,那也就是Comparable的子类了,综上,像这样类似需要通过实现某一个接口来达到预期功能的类型,使用泛型时需指定泛型的上界,并且该传入的类型必须实现该上界接口。

 

  

 

  

1.4.3泛型方法

有泛型类,那么就一定有泛型接口,泛型方法,其中泛型接口与泛型类的创建和使用是一样的,所以我们重点介绍泛型方法的创建与使用。创建泛型方法的基本语法:

 

  

方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }

 

  

例如上面实现求数组中最大元素泛型版的方法如下:

 

  

class MaxVal<T extends Comparable<T>> { public <T extends Comparable<T>> T max(T[] data) { T max = data[0]; for (int i = 0; i < data.length; i++) { if (max.compareTo(data[i]) < 0) max = data[i]; } return max; }}

对于非static修饰的静态方法, <类型形参列表>可以省略,上述代码可以变成:

 

  

class MaxVal<T extends Comparable<T>> { public T max(T[] data) { T max = data[0]; for (int i = 0; i < data.length; i++) { if (max.compareTo(data[i]) < 0) max = data[i]; } return max; }}

但是,如果是一个static修饰的静态方法,<类型形参列表>不可以省略,因为静态方法不依赖与对象,它的使用不用实例化对象,所以必须有单独的类型参数列表来指定持有的对象类型。

 

  

class MaxVal<T extends Comparable<T>> { public static <T extends Comparable<T>> T max(T[] data) { T max = data[0]; for (int i = 0; i < data.length; i++) { if (max.compareTo(data[i]) < 0) max = data[i]; } return max; }}

 

  

1.4.4类型推导

和泛型类一样,泛型方法也有类型推导的机制,如果不使用类型推导,那么泛型方法是这么使用的:

 

  

 

  使用类型推导图中画圆圈部分可以省略。

  

 

  在泛型类中没有如下的父子类关系:

  

public class MyArrayList<E> { ... } // MyArrayList<Object> 不是 MyArrayList<Number> 的父类型 // MyArrayList<Number> 也不是 MyArrayList<Integer> 的父类型

但是使用通配符这两种类是有符子类关系的。

 

  

 

  

2.通配符

 

  

2.1通配符的概念

?就是一个通配符,用与泛型的使用,与泛型不同的是,泛型T是确定的类型,传入类型实参后,它就确定下来了,而通配符更像是一种规定,规定一个范围,表示你能够传哪些参数。

 

  一个泛型类名尖括号之内仅含有一个?,就会限制这个泛型类传入的类型为Object,相当于没有限制,但是获取元素时由于不能确定具体类型,只能使用Object引用接收,所以<?>也被称为无界通配符。

  

 //使用泛型打印顺序表 public static<T> void printList1(ArrayList<T> list) { for (T x:list) { System.out.println(x); } } //使用通配符打印顺序表 public static void printList2(ArrayList<?> list) { for (Object x:list) { System.out.println(x); } }

使用泛型T能够确定传入的类型就是T类型,所以使用T类型的变量接收,而通配符?没有设置边界的情况下,默认上界是Object没有下界,为了保证安全,只能使用Object类型的变量接收。

 

  通配符是用来解决泛型无法协变的问题的,协变指的就是如果Student是Person的子类,那么List<Student>也应该是List<Person>的子类。但是泛型是不支持这样的父子类关系的。

  

 

  

2.2通配符的上界

通配符也有上界,可以限制传入的类型必须是上界这个类或者是这个类的子类。

 

  基本语法:

  

<? extends 上界> <? extends Number>//可以传入的实参类型是Number或者Number的子类

例如:

 

  

 public static void printAll(ArrayList<? extends Number> list) { for (Number n: list) { System.out.println(n); } }

我们对printAll方法的一个形参限制了类型的上界Number,所以在遍历这个顺序表的时候,需要使用Number来接收顺序表中的对象,并且使用该方法时,只能遍历输出Number及其子类的对象。

 

  

 public static void main(String[] args) { printAll(new ArrayList<Integer>());//ok printAll(new ArrayList<Double>());//ok printAll(new ArrayList<Float>());//ok printAll(new ArrayList<String>());//error }

 

  假设有如下几个类:

  

class Animal{}class Cat extends Animal{}class Dog extends Animal{}class Bird extends Animal{}

Animal是Cat,Dog,Bird类的父类,我们来看一看使用泛型和使用通配符在打印对象结果上会有什么区别?我们对这两者都设置了上界,当打印不同的对象时,到底会调用谁的toString方法。

 

  

//泛型 public static <T extends Animal> void printAnimal1(ArrayList<T> list) { for (T animal: list) { System.out.println(animal); } } //通配符 public static void printAnimal2(ArrayList<? extends Animal> list) { for (Animal animal: list) { System.out.println(animal); } }

我们先来看泛型,使用泛型指定类型后,那么指定什么类型,那它就会输出什么类型的对象,比如你指定顺序表中放的类型是Cat,那么它调用的就是Cat对象的toString方法。

 

  

 public static void main(String[] args) { Cat cat = new Cat(); Dog dog = new Dog(); Bird bird = new Bird(); //泛型 ArrayList<Cat> list1 = new ArrayList<>(); ArrayList<Dog> list2 = new ArrayList<>(); ArrayList<Bird> list3 = new ArrayList<>(); list1.add(cat); list2.add(dog); list3.add(bird); printAnimal1(list1);//Cat printAnimal1(list2);//Dog printAnimal1(list3);//Bird }

 

  再来看一看通配符,使用通配符是规定能够使用Animal及其子类,不伦你传入哪一个子类对象,都是父类的引用接收,但是具体哪一个子类,并不清楚。

  

 public static void main(String[] args) { Cat cat = new Cat(); Dog dog = new Dog(); Bird bird = new Bird(); //通配符 ArrayList<Cat> list1 = new ArrayList<>(); ArrayList<Dog> list2 = new ArrayList<>(); ArrayList<Bird> list3 = new ArrayList<>(); list1.add(cat); list2.add(dog); list3.add(bird); printAnimal2(list1);//Cat printAnimal2(list2);//Dog printAnimal2(list3);//Bird }

 

  父类引用接收子类对象发生了向上转型,当打印父类引用的子类对象时,会优先使用子类的toString方法,在介绍多态的时候也讲过这个问题,所以输出结果与使用泛型是一样的,但是泛型和通配符的效果是不一样的,泛型是你传入什么类型,那这个类就会持有什么类型的对象,而通配符是规定一个范围,规定你能够传哪一些类型。

  通配符的上界是支持如下的父子类关系的,而泛型的上界不支持:

  

MyArrayList<? extends Number> 是 MyArrayList <Integer>或者 MyArrayList<Double>的父类类型 MyArrayList<?> 是 MyArrayList<? extends Number> 的父类型

对于通配符的上界有个特点,先说结论,使用通配符上界可以读取数据,但是并不适合写入数据,因为不能确定类所持有的对象具体是什么。

 

  

 public static void main(String[] args) { ArrayList<Integer> arrayList1 = new ArrayList<>(); ArrayList<Double> arrayList2 = new ArrayList<>(); arrayList1.add(10); List<? extends Number> list = arrayList1; System.out.println(list.get(0));//ok Integer = list.get(0);//error因为不能确定list所持有的对象具体是什么 list.add(2);//error因为不能确定list所持有的对象具体是什么,为了安全,这种情况Java不允许插入元素 }

 

  因为从list获取的对象类型一定Number或者Number的子类,所以可以使用Number引用来获取元素,但是插入元素时你并不能确定它到底是哪一种类型,为了安全,使用通配符上界的list不允许插入元素。

  

 

  

2.3通配符的下界

与泛型不同,通配符可以拥有下界,语法层面上与通配符的上界的区别是讲关键字extends改为super。

 

  

<? super 下界> <? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型

既然是下界那么通配符下界与上界对传入类的规定是相反的,即规定一个泛型类只能传入下界的这个类类型或者这个类的父类类型。比如<? super Integer>代表 可以传入的实参的类型是Integer或者Integer的父类类型(如Number,Object)

 

  

 public static void printAll(ArrayList<? super Number> list) { for (Object n: list) {//此处只能使用Object接收,因为传入的类是Number或者是Number的父类 System.out.println(n); } }
 public static void main(String[] args) { printAll(new ArrayList<Number>());//ok printAll(new ArrayList<Object>());//ok printAll(new ArrayList<Double>());//error printAll(new ArrayList<String>());//error printAll(new ArrayList<Integer>());//error }

 

  同理通配符的下界也是满足像下面这种父子类关系的。

  

MyArrayList<? super Integer> 是 MyArrayList<Integer>的父类类型 MyArrayList<?> 是 MyArrayList<? super Integer>的父类类型

总结:

 

  ?是? extends ....和? super ....的父类,看通配符之间的父子类关系,最关键的是看通配符所规定的范围,判断父子类是根据这个范围来判断的。

  通配符的下界也有一个特点,那就是它能够允许写入数据,当然能够写入的数据对象是下界以及下界的子类,但是并不擅长读数据,与通配符的上界相反。

  

 public static void main(String[] args) { ArrayList<? super Animal> list = new ArrayList<Animal>(); ArrayList<? super Animal> list2 = new ArrayList<Cat>();//编译报错,list2只能引用Animal或者Animal父类类型的list list.add(new Animal());//添加元素时,只要添加的元素的类型是Animal或者Animal的子类就可以 list.add(new Cat()); Object s2 = list.get(0);//可以 ArrayList<? super Animal> list3 = new ArrayList<Object>(); Cat s1 = list3.get(0);//error因为构造对象时可以构造Animal父类类型的ArrayList,取出的对象不一定是Animal或者Animal的子类 }

 

  对于这个栗子添加元素时,只要添加的元素的类型是Animal或者Animal的子类就可以,获取元素时,只能使用Object引用接收,不能使用其他的引用接收,因为因为构造对象时可以构造Animal父类类型的ArrayList,虽然可以插入Animal以及其子类对象,但取出的对象不能保证是Animal或者Animal的子类。

  关于泛型和通配符就介绍到这里了,这两个概念是非常抽象而且难懂的,而且用的也不多,因为基本上以后你也没机会用了,因为泛型通配符在编写类似Java或者其他语言源码的时候才用得到,学习泛型通配符的主要目的是能够读懂源码,看得懂其他人写的代码。

  哎!写关于java语法的用法的时候真的是一头雾水啊!

  

 

  

总结

到此这篇关于Java泛型与通配符的文章就介绍到这了,更多相关Java泛型与通配符内容请搜索盛行IT以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT!

 

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: