首先,泛型的这个概念是在java5之后才有的,java5增加泛型支持很大程度上是为了让集合记住其元素的数据类型。在没有泛型之前,一旦把一个对象“丢进”Java 集合,集合就会忘记对象的的类型,把所有的对象当成Object类型处理。当程序从集合中取出对象后,就需要进行强制类型转换
,这种强制类型转换不仅使代码臃肿,而且容易引起ClassCastExeception异常。
泛型的本质是为了参数化类型
(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类
、泛型接口
、泛型方法
。
泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型),例如下面的代码:
实例代码:
import java.util.ArrayList;
import java.util.List;
/**
* @author: 随风飘的云
* @describe:编译时不检查类型的异常
* @date 2022/08/15 18:12
*/
public class ListErr {
public static void main(String[] args){
// 创建一个只想保存字符串的List集合
List strList = new ArrayList();
strList.add("abcd");
strList.add("hhhhh");
// "不小心"把一个Integer对象"丢进"了集合
strList.add(5); // 1
strList.forEach(str -> System.out.println(((String)str).length())); // 2
//如果是下面的写法,那么list中只能放String, 不能放其它类型的元素,
//如果添加了其他对象,那么是无法通过编译的。
//List list = new ArrayList();
}
}
结果: 程序创建了 一个 List 集合 ,而且只希望该 List 集合保存字符串对象,但程序不能进行任何限制,如果程序在①处"不小心"把一个Integer 对象"丢进"了List 集合中,这将导致程序在②处引发ClassCastException 异常,因为程序试图把一个Integer 对象转换为 String 类型。
在java5中添加了泛型的定义,解决了集合对于对象存储和取出的类型转换问题,除此之外,java5 添加了泛型定义,支持了代码的复用
:适用于多种数据类型执行相同的代码
首先,如果没有java5 的泛型支持,那么实现一个简单的加法需要这么多代码。
/**
* @author: 随风飘的云
* @describe:如果没有泛型支持
* @date 2022/08/15 18:18
*/
public class TestAlgorith {
private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
public static void main(String[] args) {
add(1,2);
add(1.0,2);
add(1.2, 2);
}
}
如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:
实例代码:
/**
* @author: 随风飘的云
* @describe:有泛型支持
* @date 2022/08/15 18:18
*/
public class TestAlgorith {
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
public static void main(String[] args) {
add(1,2);
add(1.0,2);
add(1.2, 2);
}
}
程序定义了 一个带泛型声明的 Apple< T >类(不要理会这个泛型形参是否具有实际意义) ,使用Apple< T >类时就可为 T 形参传入实际类型,这样就可以生成如 Apple< String > 、 Apple< Double >等等形式的多个逻辑子类(物理上并不存在) 。
简单泛型类实例代码:
/**
* @author: 随风飘的云
* @describe:泛型类
* @date 2022/08/15 20:31
*/
public class Apple<T> { //此处可以随便写标识符号,T是type的简称
// 使用T类型定义实例变量,即由外部指定
private T info;
public Apple(){}
// 下面方法中使用T类型来定义构造器,设置的值的类型是由外部指定
public Apple(T info) {
this.info = info;
}
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return this.info;
}
public static void main(String[] args) {
// 由于传给T形参的是String,所以构造器参数只能是String
Apple<String> a1 = new Apple<>("苹果");
System.out.println(a1.getInfo());
// 由于传给T形参的是Double,所以构造器参数只能是Double或double
Apple<Double> a2 = new Apple<>(5.67);
System.out.println(a2.getInfo());
}
}
多元泛型类实例代码:
class Notepad<K,V>{ // 此处指定了两个泛型类型
private K key ; // 此变量的类型由外部决定
private V value ; // 此变量的类型由外部决定
public K getKey(){
return this.key ;
}
public V getValue(){
return this.value ;
}
public void setKey(K key){
this.key = key ;
}
public void setValue(V value){
this.value = value ;
}
}
public class GenericsDemo09{
public static void main(String args[]){
Notepad<String,Integer> t = null ; // 定义两个泛型类型的对象
t = new Notepad<String,Integer>() ; // 里面的key为String,value为Integer
t.setKey("汤姆") ; // 设置第一个内容
t.setValue(20) ; // 设置第二个内容
System.out.print("姓名;" + t.getKey()) ; // 取得信息
System.out.print(",年龄;" + t.getValue()) ; // 取得信息
}
}
当创建了带泛型声明的接口、父类之后 ,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些接口 、父类时不能再包含泛型形参 。 例如:下面的代码是错误的
//定义类 A 继承 Apple 类, Apple 类不能跟泛型形参
public class A extends Apple<T>{ }
如果想从 Apple 类派生一个子类,则可以改为如下代码:
//使用 Apple 类时为 T 形参传入 String 类型
public class A extends Apple< String >{}
调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时也可以不为泛型形参传入实际的类型参数,即下面代码也是正确的 。
//使用 Apple 类时,没有为 T 形参传入实际的类型参数
public class A extends Apple
如果从 Apple类派生子类 ,则在 Apple 类中所有使用 T 类型的地方都将被替换成 String 类型,即它的子类将会继承到 String getInfo()和 void setlnfo(String info)两个方法,如果子类需要重写父类的方法,就必须注意这一点。
/**
* @author: 随风飘的云
* @describe:泛型类派生子类
* @date 2022/08/15 20:45
*/
public class A1 extends Apple<String> {
// 正确重写了父类的方法,返回值
// 与父类Apple的返回值完全相同
public String getInfo() {
return "子类" + super.getInfo();
}
// // 下面方法是错误的,重写父类方法时返回值类型不一致
// public Object getInfo(){
// return "子类";
// }
}
如果使用 Apple 类时没有传入实际的类型(即使用原始类型), Java 编译器可能发出警告:使用了未经检查或不安全的操作一这就是泛型检查的警告。如果希望看到该警告提示的更详细信息,则可以通过为 Javac 命令增加-Xlint:unchecked 选项来实现 。 此时,系统会把 Apple类里的 T 形参当成Object 类型处理 。
/**
* @author: 随风飘的云
* @describe:泛型警告
* @date 2022/08/15 20:48
*/
public class A2 extends Apple {
// 重写父类的方法
public String getInfo() {
// super.getInfo()方法返回值是Object类型,
// 所以加toString()才返回String类型
return super.getInfo().toString();
}
public static void main(String[] args) {
Apple<String> a1 = new Apple<>("华为");
System.out.println(a1.getInfo());
// 由于传给T形参的是Double,所以构造器参数只能是Double或double
Apple<Double> a2 = new Apple<>(123.34);
System.out.println(a2.getInfo());
}
}
简单的泛型接口:
interface Info<T>{ // 在接口上定义泛型
public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型
}
class InfoImpl<T> implements Info<T>{ // 定义泛型接口的子类
private T var ; // 定义属性
public InfoImpl(T var){ // 通过构造方法设置属性内容
this.setVar(var) ;
}
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
}
public class GenericsDemo24{
public static void main(String arsg[]){
Info<String> i = null; // 声明接口对象
i = new InfoImpl<String>("汤姆") ; // 通过子类实例化对象
System.out.println("内容:" + i.getVar()) ;
}
}
泛型方法,是在调用方法的时候指明泛型的具体类型,具体请查看下面的图:(图片参考)
调用泛型方法语法格式
当使用一个泛型类时 (包括声明变量和创建对象两种情况) , 都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数 , 编译器就会提出泛型警告。假设现在需要定义一个方法 , 该方法里有一个集合形参,集合形参的元素类型是不确定的,考虑下面的代码:
public void Test(List c){
for (int i = 0; i < c.size(); i++) {
System.out.println(c.get(i));
}
}
上面的代码定义没有问题,只是一个遍历list集合的代码,问题是上面程序中List 是一个有泛型声明的接口 , 此处使用 List 接口时没有传入实际类型参数,这将引起泛型警告 。 为此,考虑为List 接口传入实际的类型参数一 因为 List 集合里的元素类型是不确定的,将上面的代码改为下面的这种形式:
public void Test(List<Object> c){
for (int i = 0; i < c.size(); i++) {
System.out.println(c.get(i));
}
}
但是又有一个问题,如果我使用这个方法传入的参数是这样子的,那会不会有问题呢?
//创建一个 List对象
List<String> strList = new ArrayList <> ();
//将 strList 作为参数来调用前面的 test 方法
Test(strList) ; //
这当然有问题了,且看实例代码:
import java.util.ArrayList;
import java.util.List;
/**
* @author: 随风飘的云
* @describe:
* @date 2022/08/15 21:25
*/
public class test {
public static void Test(List<Object> c){
for (int i = 0; i < c.size(); i++) {
System.out.println(c.get(i));
}
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("ss");
Test(list);
}
}
结果:
那该怎么解决这个问题呢?使用类型通配符,什么是类型通配符?类型通配符是一个问号(?) ,将一个问号作为类型实参传给 List 集合,写作: List>(意思是元素类型未知的 List ) 。 这个问号(?)被称为通配符,它的元素类型可以匹配任何类型 。 那上面有错误的代码就可以改成这样子的格式了。
import java.util.ArrayList;
import java.util.List;
/**
* @author: 随风飘的云
* @describe:
* @date 2022/08/15 21:25
*/
public class test {
public static void Test(List<?> c){
for (int i = 0; i < c.size(); i++) {
System.out.println(c.get(i));
}
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("ss");
Test(list);
}
}
当直接使用 List< ?>这种形式时,即表明这个 List 集合可以是任何泛型 List的父类。但还有一种特殊的情形,程序不希望这个 List>是任何泛型 List 的父类,只希望它代表某一类泛型 List 的父类。
先来看下面的代码:
// 定义一个抽象类Shape
public abstract class Shape {
public abstract void draw(Canvas c);
}
// 定义Shape的子类Circle
public class Circle extends Shape {
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c) {
System.out.println("在画布" + c + "上画一个圆");
}
}
// 定义Shape的子类Rectangle
public class Rectangle extends Shape {
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c) {
System.out.println("把一个矩形画在画布" + c + "上");
}
}
上面定义了 三个形状类,其中 Shape 是一个抽象父类 , 该抽象父类有两个子类 : Circle 和 Rectangle 。接下来定义一个 Canvas 类,该画布类可以画数量不等的形状 (Shape 子类的对象) 。那么定义一个Canvas类如下:
import java.util.ArrayList;
import java.util.List;
public class Canvas {
// 同时在画布上绘制多个形状
public void draw(List<Shape> shapes){
for (Shape s : shapes){
s.draw(this);
}
}
// 同时在画布上绘制多个形状,使用被限制的泛型通配符
public void drawAll(List<? extends Shape> shapes) {
for (Shape s : shapes) {
s.draw(this);
}
}
public static void main(String[] args) {
List<Circle> circleList = new ArrayList<Circle>();
Canvas c = new Canvas();
// 由于List并不是List的子类型,
// 所以下面代码引发编译错误
// c.draw(circleList);
c.drawAll(circleList);
}
}
需要注意的是上面的 draw()方法的形参类型是 List< Shape > ,而 List< Circle >并不是 List< Shape >的子类型 ,因此,下面代码将引起编译错误。
List<Circle> circleList = new ArrayList<>( );
Canvas c = new Canvas ();
//不能把 List 当成 List使用,所以下面代码引起编译错误
c.draw(circleList);
那有什么方法可以解决呢?有!把 List< Circle >对象当成 List extends Shape>使用。即List extends Shape>可以表示 List< Circle > 、 List< Rectangle >的父类,而且这个Shape类被称之为类型通配符的上限。如上面的drawAll(List extends Shape> shapes)方法代码。
需要注意的是,由于程序无法确定这个受限制的通配符的具体类型,所以不能把 Shape 对象或其子类的对象加入这个泛型集合中 ,例如下面的代码是错误的:
public void addRectangle(List<? extends Shape> shapes){
//下面代码引起编译错误
shapes .add(O , new Rectangle());
}
总的来说,这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型) ,不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型) 。
配符的下限用 super 类型>的方式来指定,通配符下限的作用与通配符上限的作用恰好相反 。
指定通配符的下限就是为了支持类型型变。 比如 Foo 是 Bar 的子类,当程序需要一个 A< ? super Bar >变量时,程序可以将 A< Foo > 、 A< Object >赋值给 A< ? super Bar >类型的变量,这种型变方式被称为逆变 。
对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类) ,从集合中取元素时只能被当成Object 类型处理(编译器无法确定取出的到底是哪个父类的对象) 。例如:
import java.util.ArrayList;
import java.util.List;
public class MyUtils {
// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
public static <T> T copy(List<? super T> dest, List<T> src){
T last = null;
for (T ele : src) {
last = ele;
// 逆变的泛型集合添加元素是安全的
dest.add(ele);
}
return last;
}
public static void main(String[] args) {
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
li.add(12);
// 此处可准确的知道最后一个被复制的元素是Integer类型
// 与src集合元素的类型相同
Integer last = copy(ln , li); // ①
System.out.println(ln); // [5,12]
System.out.println(last); // 12
}
}
先再来看一个实例代码:
private <E extends Comparable<? super E>> E max(List<? extends E> e1){
if (e1 == null){
return null;
}
//迭代器返回的元素属于 E 的某个子类型
Iterator<? extends E> iterator = e1.iterator();
E result = iterator.next();
while (iterator.hasNext()){
E next = iterator.next();
if (next.compareTo(result) > 0){
result = next;
}
}
return result;
}
上述代码中的类型参数 E 的范围是
,我们可以分步查看:
extends Comparable<…>
(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E>
要对E
进行比较,即 E
的消费者,所以需要用 super
List< ? extends E>
表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大如果有多个限制,使用&
解决,如代码所示:
public class Client {
//工资低于2500元的上斑族并且站立的乘客车票打8折
public static <T extends Staff & Passenger> void discount(T t){
if(t.getSalary()<2500 && t.isStanding()){
System.out.println("恭喜你!您的车票打八折!");
}
}
public static void main(String[] args) {
discount(new Me());
}
}
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
首先,我们泛型数组相关的申明:
//编译错误,非法创建
List<String>[] list11 = new ArrayList<String>[10];
//编译错误,需要强转类型
List<String>[] list12 = new ArrayList<?>[10];
//OK,但是会有警告
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10];
//编译错误,非法创建
List<?>[] list14 = new ArrayList<String>[10];
//OK
List<?>[] list15 = new ArrayList<?>[10];
//OK,但是会有警告
List<String>[] list6 = new ArrayList[10];
使用场景:
public class GenericsDemo30{
public static void main(String args[]){
Integer i[] = fun1(1,2,3,4,5,6) ; // 返回泛型数组
fun2(i) ;
}
public static <T> T[] fun1(T...arg){ // 接收可变参数
return arg ; // 返回泛型数组
}
public static <T> void fun2(T param[]){ // 输出
System.out.print("接收泛型数组:") ;
for(T t:param){
System.out.print(t + "、") ;
}
}
}