为什么有泛型?

首先,泛型的这个概念是在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<String> list = new ArrayList<String>();



    }
}

结果: 程序创建了 一个 List 集合 ,而且只希望该 List 集合保存字符串对象,但程序不能进行任何限制,如果程序在①处"不小心"把一个Integer 对象"丢进"了List 集合中,这将导致程序在②处引发ClassCastException 异常,因为程序试图把一个Integer 对象转换为 String 类型。

java String 强转T泛型 java泛型强制转换_List

引入泛型的意义

在java5中添加了泛型的定义,解决了集合对于对象存储和取出的类型转换问题,除此之外,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);
    }
}

java5支持的泛型定义

如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个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<String>的返回值完全相同
    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()) ;  
    }  
}

泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型,具体请查看下面的图:(图片参考)

java String 强转T泛型 java泛型强制转换_java String 强转T泛型_02


调用泛型方法语法格式

java String 强转T泛型 java泛型强制转换_java String 强转T泛型_03

  1. 说明一下,定义泛型方法时,必须在返回值前边加一个< T >,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。
  2. Class< T >的作用就是指明泛型的具体类型,而Class< T >类型的变量c,可以用来创建泛型类的对象。
  3. 为什么要用变量 c 来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量 c 的 newInstance 方法去创建对象,也就是利用反射创建对象。
  4. 泛型方法要求的参数是Class< T >类型,而Class.forName()方法的返回值也是Class< T >,因此可以用Class.forName()作为参数。其中,forName()方法中的参数是何种类型,返回的Class< T >就是何种类型。在本例中,forName()方法中传入的是User类的完整路径,因此返回的是Class< User >类型的对象,因此调用泛型方法时,变量 c 的类型就是Class< User >,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。
  5. 当然,泛型方法不是仅仅可以有一个参数Class,可以根据需要添加其他参数。
  6. 为什么要使用泛型方法呢?因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。

类型通配符

当使用一个泛型类时 (包括声明变量和创建对象两种情况) , 都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数 , 编译器就会提出泛型警告。假设现在需要定义一个方法 , 该方法里有一个集合形参,集合形参的元素类型是不确定的,考虑下面的代码:

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<String>对象
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);
    }
}

结果:

java String 强转T泛型 java泛型强制转换_开发语言_04


那该怎么解决这个问题呢?使用类型通配符,什么是类型通配符?类型通配符是一个问号(?) ,将一个问号作为类型实参传给 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<Circle>并不是List<Shape>的子类型,
        // 所以下面代码引发编译错误
        // c.draw(circleList);
        c.drawAll(circleList);
        
    }
}

需要注意的是上面的 draw()方法的形参类型是 List< Shape > ,而 List< Circle >并不是 List< Shape >的子类型 ,因此,下面代码将引起编译错误。

List<Circle> circleList = new ArrayList<>( );
Canvas c = new Canvas ();
//不能把 List<Circle> 当成 List<Shape>使用,所以下面代码引起编译错误
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 的范围是<E extends Comparable<? super E>>,我们可以分步查看:

  • 要进行比较,所以 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 + "、") ;  
        }  
    }  
}

文章参考