java泛型详解(1)

本文深入解析泛型的概念,包括泛型类、接口、方法的使用,以及通配符在泛型中的作用,强调泛型提升代码重用性和安全性的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么要使用泛型?

一般的类和方法,只能使用具体的类型;要么是基本类型,要么是自定义类。如果我有这种需求:可以应用于多种类型的代码。该怎么办?

  1. 创建多个class文件,从而可以创建多个不同类型的对象;缺点很明显:代码臃肿,重用性不高
  2. 创建一个类文件,给这个类中的成员变量设置Object数据类型;编译期可能会通过,运行时有可能发生异常ClassCastException

一般的类和方法,只能使用具体类型,要么是基本类型,要么是自定义类型。这样的类重用性不高。泛型的核心就是:参数化类型使代码可以应用于多种类型。

通过下面这个例子来说明什么叫参数化类型:

public class Test<T>{
	T obj;
	public  void set(T obj) {
		this.obj=obj;
	}
	public T get() {
		return obj;
	}
	public static void main(String args[]) {
		Test<String> t=new Test<String>();
		t.set("123");
		String s=t.get();
		System.out.println(s);
		Test<Integer> t2=new Test<Integer>(12);
		Integer i=t2.get();
		System.out.println(i);
	}
}

从上面的例子我们可以很明显的看见,可以应用于String和Integer类型。通过泛型类我们将类型参数化,创建类时我先不指定这个类持有什么类型,当我们确定想要的类型时,显式定义这个类的实例是某个类型。这就是参数化类型的含义。这样的好处代码的重用性提高了。

Object和泛型

这时我们会想是不是可以用Object来代替T,因为Object是所有类的根类,就像是继承的关系,父类的引用指向子类的实例,似乎也可以达到参数化类型的目的。看下面代码:

public class Test{
	Object obj;
	public  void set(Object obj) {
		this.obj=obj;
	}
	public Object get() {
		return obj;
	}
	public static void main(String args[]) {
		Test t=new Test();
		t.set("123");
		String s=(String)t.get();
		System.out.println(s);
	}
}

通过上面的代码我们发现当我们用类型转换的时候需要强制定义类型,这有个很明显缺点就是必须要我们清楚的知道我们定义的是什么类型。因为编译器对于强制转换可能不会提示错误。

这就说明了泛型的一个好处就是类型安全,他的强制转换时自动和隐式的,编译器会自动进行类型检查。
当我们想要持有一个不是我们指定的类型,就会报错。

  1. 与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
  2. 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。(如果你将类型设置为Object的话不会类型检测)所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
  3. 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,程序员能够一目了然猜测出代码要操作的数据类型。

泛型类

public class Test <T>{}

用一个 放在类名后面。然后在使用的时候,再用实际类型替换类型参数,这里面的T只是一个占位符来表示类型参数,可以换成任意的E,V之类的。

当然后面的类型参数也可以设置为多个,当你想要这样的功能的时候:仅调用一次方法就能返回多个对象,因此我们就可以传递多个类型参数,这个叫元组,它是将一组对象直接打包存储于其中一个单一对象。

public class TwoTuple<A,B>{
    public A a;
    public B b;
    public TwoTuple(A a,B b){
        this.a=a;
        this.b=b;
    }
}
/*我们可以利用继承机制类实现长度更长的元组。*/
class ThreeTuple<A,B,C> extends TwoTuple{
    public C c;
    public ThreeTuple(A a,B b,C c){
        super(a,b);
        this.c=c;
    }
}

class TupleTest{
/*将元组设置为方法的返回类型*/
    static TwoTuple<String,Integer> f(){
        return new TwoTuple<String,Integer>("hi",18);
    }
}

有许多原因造成泛型类的出现,其中最引人注目的一个原因就是:为了创造容器类。通过泛型来指定容器该持有什么类型对象。

package 泛型;
//容器泛型
/*假设需要一个持有特定类型的对象的列表,每次调用select()时,它会随机给我们一个元素,
 *如果我们希望可以以此构建一个可以应用于各种类型的对象的工具,就可以用到泛型*/
import java.util.*;
public class RandomList<T> {
	private ArrayList<T> storage=new ArrayList<T>();
	Random rand=new Random(47);
	public void add(T item) {
		storage.add(item);
	}
	public T select() {
		return storage.get(rand.nextInt(storage.size()));
	}
	public static void main(String args[]) {
		RandomList<String> rs=new RandomList<String>();
		for(String s:("The quick brown fox jumped over"+"the lazy brown dog").split(" "))
			rs.add(s);
		for(int i=0;i<11;i++)
			System.out.println(rs.select()+" ");
	}
}

泛型接口

表示方法和泛型类一样,Thinking in java358里面有个很好的例子:生成器,这是一种专门负责创建对象的类,是工厂方法的一种应用。

public interface Generator<T>{
    T next();//将返回类型参数化,从而返回不同类型的对象
}
package 泛型接口;
//工具类
public class Coffee{
	private static int count=0;
	private final int id=count++;
	public String toString() {
		return getClass().getSimpleName()+" "+id;
	}
}
class Latte extends Coffee{}
class Mocha extends Coffee{}
class Cappuccino extends Coffee{}
class Americano extends Coffee{}
class Breve extends Coffee{}


package 泛型接口;
import java.util.*;

//生成器:这是一种专门创建对象的类,无需额外的信息就知道如何创建新对象
public class CoffeeGenerator implements Generator<Coffee>,Iterable<Coffee>{
	//只有继承Iterable接口才可以使用foreach语句(数组可以直接使用)
	private Class<?> types[]= {Latte.class,Mocha.class,Cappuccino.class,Americano.class,Breve.class};
	private static Random rand=new Random(47);
	public CoffeeGenerator() {}
	private int size=0;
	public CoffeeGenerator(int ss) {//末端哨兵的作用
		this.size=ss;
	}
	public Coffee next() {//生成器方法
		try {
			return (Coffee)types[rand.nextInt(types.length)].newInstance();//newinstance默认是Obect所以得强制转换
		} catch(Exception e) {
			throw new RuntimeException(e);
		}
	}
	public Iterator<Coffee> iterator(){
		return new Iterator<Coffee>() {//这里使用匿名内部类,因为继承实现Iterable接口要实现Iterator iterator()方法
			int count=size+1;
			public boolean hasNext() {//是你们那个内部类继承Iterator类,就需要实现内部的hasNext()和next()方法
				return count>0;
			}
			public Coffee next() {
				count--;
				return CoffeeGenerator.this.next();
			}
		};
	}
	public static void main(String args[]) {
		CoffeeGenerator gen=new CoffeeGenerator();
		for(int i=0;i<5;i++)
			System.out.println(gen.next());
		for(Coffee c:new CoffeeGenerator(5))
			/*foreach语句的内部实现是用到了Iterator接口中的next()和hasNext()方法,CoffeeIterator实现了Iterator接口
			     重写了其中的方法。先判断hasNext在next,所以CoffeeGenerator的参数构造方法有着末端哨兵的作用
			   通过初始化size的值,size和内部类中的Count联系起来,使用foreach语句会调用next方法当count--<0时跳出循环*/
			System.out.println(c);
	}
}

这里的有参数的构造方法是“末端哨兵”:1.初始化size;2.count=size;3.next(){count --};4.hasNext(){count>0}从而停止创建对象。

泛型方法

将泛型用在类中和方法的思想是一样的,泛型方法将返回类型参数化,从而传入不同的类型参数。

 public class GenericMethod {
	public <T>void f(T x) {//泛型方法
		System.out.println(x.getClass().getName());
	}
//当使用泛型类时,必须在创建对象的时候自定类型参数的值,而使用泛型方法时,通常不必指明参数类型,因为编译器会为我们找出具体参数
	public static void main (String args[]) {
		GenericMethod g=new GenericMethod();
		g.f(123);
		g.f(" s");
		g.f(1.0f);
	}
}

output:
java.lang.Integer
java.lang.String
java.lang.Float

从上面的代码我们可以看到 GenericMethod并不是一个泛型类。没错,泛型方法是独立于泛型类的,不管类不是不泛型类,我们都可以定义泛型方法。这里我们得注意一点: 没有静态泛型字段,要想static具有泛型能力就必须是泛型方法。

public class Test<T>{
    static T x;//error
    static <T> void f(T x){}//OK
}

从上面的代码可以看出:我似乎没有定义x的具体类型,他自动的会输出正确的类型。这就是泛型方法的好处:我们不必指明参数类型,因为编译器会自动的为我们找出具体的类型。这就是类型参数推断。

public void f(T x) {
		System.out.println(x.getClass().getName());
	}

如果我们将f()改成上面一样,似乎能达到相同的作用,只不过麻烦点。直接看下面代码:

class Mouse{}
class Cat{}
//添加两个具体类型测试
public static void main (String args[]) {
		GenericMethod g=new GenericMethod();
		g.f(new Mouse());
		g.f(new Cat());//error
	}

f()只能接受一个具体类型。这就好像泛型类一样,当你指定一个具体的类型时,你就不能持有其他类型了,除非你再定义一个泛型类对象。

注意: 当泛型方法能够满足我们的需求的时候我们就尽量使用泛型方法,而不是无论什么都使用泛型类或者接口。因为泛型方法更简单更清楚。

这时我们就可以将上面说的泛型接口中的生成器改变一下:

import java.util.*;
import 泛型接口.CoffeeGenerator;
import 泛型接口.Generator;
import 泛型接口.Coffee;
public class Generators{//这里可以和泛型接口中的Generator比较
	/*Generator是个泛型接口 当你指定一具体的类为类型参数时,就只能创建这个类型参数的对象,不能转换为其他类型
	 * 
	 * 但是这里我并没有将这个类设置为泛型类,我只是将生成器方法设置为泛型方法,这样我就不必指明参数类型(编译器会自动为我们找出具体类型)
	 * 因此我的fill方法可以设置为任意类型(只要实现了Generator)
	 * 这里我们不将fill设置为泛型方法会出错(并不是static的问题,如果将static去掉也同样出错)*/
	//对于static方法而言,他无法访问泛型类的类型参数,如果static方法需要泛型能力就必须将这个方法设置为泛型方法。
	public static <T>Collection<T> fill(Collection<T> coll,Generator<T> gen,int n){
		for(int i=0;i<n;i++)
			coll.add(gen.next());
		return coll;
	}
	public static void main(String args[]) {
		Collection<Coffee> coffee=fill(new ArrayList<Coffee>(),new CoffeeGenerator(),4);
		for(Coffee c:coffee)
			System.out.println(c);
	}
	
}

泛型参数的作用域

class A<T> { ... }中T的作用域就是整个A;
public <T> func(...) { ... }中T的作用域就是方法func;
class A<T> {
    // A已经是一个泛型类,其类型参数是T
    public static <T> void func(T t) {
    // 再在其中定义一个泛型方法,该方法的类型参数也是T
    }
}
//当上述两个类型参数冲突时,在方法中,
方法的T会覆盖类的T,即和普通变量的作用域一样
,内部覆盖外部,外部的同名变量是不可见的

通配符在泛型中的使用:为了指定泛型中的类型范围


<?>无界通配符:放泛型接受未知的类型。

我们不能对List<?>使用add方法(add()null除外)为什么:因为我们并不知道List里面是什么类型,只有null是所有引用数据类型都具有的元素

public static void addTest(List<?> list) {
    Object o = new Object();
    // list.add(o); // 编译报错
    // list.add(1); // 编译报错
    // list.add("ABC"); // 编译报错
    list.add(null);
}

我们同样不能使用get方法赋值给一个类型对象除了Object外:因为我们并不知道get的返回类型是什么,但是Object是所有类型的父类。Integer in=list.get(0)这是错的。

 public static void getTest(List<?> list) {
     // String s = list.get(0); // 编译报错
     // Integer i = list.get(1); // 编译报错
     Object o = list.get(2);
 }
<?extends T>上边界通配符

有一点我们需要记住的是, List<?extends E>不能使用add方法, 请看如下代码:

 public static void addTest2(List<? extends Number> l) {
     // l.add(1); // 编译报错
     // l.add(1.1); //编译报错
     l.add(null);
 }

原因很简单, 泛型<? extends E>指的是E及其子类, 这里传入的可能是Integer, 也可能是Double, 我们在写这个方法时不能确定传入的什么类型的数据。

但是我们可以使用get方法因为上边界是Number类型,我们不管掺入什么类型都是Number类型的子类型。

<? super Integer>下边界通配符

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

public static void main(String[] args) {
    List<Object> list1 = new ArrayList<>();
    addNumbers(list1);
    System.out.println(list1);
    List<Number> list2 = new ArrayList<>();
    addNumbers(list2);
    System.out.println(list2);
    List<Double> list3 = new ArrayList<>();
    // addNumbers(list3); // 编译报错
}

我们看到, List<?super E>是能够调用add方法的, 因为我们在addNumbers所add的元素就是Integer类型的, 而传入的list不管是什么, 都一定是Integer或其父类泛型的List, 这时add一个Integer元素是没有任何疑问的.

但是, 我们不能使用get方法, 请看如下代码:

 public static void getTest2(List<? super Integer> list) {
     // Integer i = list.get(0); //编译报错
     Object o = list.get(1);
 }

这个原因也是很简单的, 因为我们所传入的类都是Integer的类或其父类, 所传入的数据类型可能是Integer到Object之间的任何类型, 这是无法预料的, 也就无法接收. 唯一能确定的就是Object, 因为所有类型都是其子类型.

类型参数T和通配符?的区别

最主要的区别就是:“T”是用在定义泛型类的时候,“?”是用来使用泛型类的时候

  1. 定义泛型类
class Test<T>{
    private T a;
    private T b;
    public T f(){}
    public void ff(T c){}
}

这里为什么用“T”:泛型的作用是参数化类型,也就是说我们在使用的时候必须给他具体的类型,就好像形参和实参。这里的T起到一个限定的作用。Test<String>将字段a,b限定为String类型。Test中用到T的地方:字段,方法的返回类型,方法的参数类型。因此无限定的通配符不能用来定义类的。

class Test<?>{
    private ? a; 
}

用?来表示类型参数的限定肯定是不行的,?表示的是任何事物

  1. 定义泛型方法也是用到类型参数T,作用和泛型类一样
  2. 当对已经存在的泛型,我们不想给她一个具体的类型做为类型参数,我们可以给她一个不确定的类型作为参数,(前提是这个泛型必须已经定义)
    Test<?>我们可以把这个参数类型加以限制
    Test<?super B> 具体的用法上面已经有说。

参考:https://www.zhihu.com/question/31429113

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值