Java 泛型机制学习

本文深入浅出地介绍了Java泛型的基本概念,包括泛型的作用、如何在类和接口中定义泛型,以及如何使用泛型增强程序的健壮性和灵活性。此外,还详细解释了类型通配符的使用方法及其上限和下限的设定。

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

有时候听别人说泛型泛型的,心想啥是泛型啊,当时就不能愉快的交流了。所以今天赶紧看看啥是泛型,提高一下自己。

泛型的概念:所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态的指定(即传入实际的类型参数)。Java 5改写了集合框架中的全部类和接口,为这些类和接口增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。

为什么引入泛型?使用泛型可以使错误在编译时被探测到,从而增加程序的健壮性。

平时使用泛型最多的地方就是集合了。

下面这一段代码

List strList = new ArrayList();
strList.add("hello world");
strList.add(3);
String s = (String) strList.get(1);//强转成String类型
System.out.println(s);

我们没有使用泛型,我们声明了一个strList,添加了 String 类型的数据,如果我们一不留神添加了一个Integer类型的数据,编译的时候是没有报错的,但是在运行的时候就出错了。

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

如果我们使用泛型的话,就可以在编译期间发现这种错误,从而增加程序的健壮性。

List<String> strList = new ArrayList();
strList.add("hello world");
//这句代码编译通不过。
strList.add(3);

这个声明的List只能保存String类型的数据。而且从集合中取数据的时候并不需要强制类型转换。

 List<String> strList = new ArrayList();
 strList.add("hello world");
 strList.add("hello 2017");
 String s = strList.get(1);//不用强制类型转换,可以愉快的取出String。
 System.out.println(s);

定义泛型类、泛型接口

1 定义泛型类

//定义Apple类使用泛型声明
public class Apple<T> {

    //使用T类型形参定义实例变量
    private T info;

    public Apple() {
    }

    //使用T类型形参定义构造函数
    public Apple(T info) {
        this.info = info;
    }

    public T getInfo() {
        return info;
    }

    public void setInfo(T info) {
        this.info = info;
    }
}

当我们使用泛型类的时候,应该传入一个具体类型参数

public static void main(String args[]) {
    //由于传给T形参的是String,所以构造函数参数只能是String
    Apple<String> a1 = new Apple<>("hello apple");
    //由于传给T形参的是Double类型,所以构造函数参数只能是Double或者double
    Apple<Double> a2 = new Apple<>(3.4);
    System.out.println(a1.getInfo());
    System.out.println(a2.getInfo());
}

如果我们不指定一个具体的类型参数,像下面这样,编辑器会有一个未检查警告。

Apple a3 = new Apple("hello apple");

2定义泛型接口

//这里声明了两个类型参数 K ,V
public interface Pair<K, V> {

    K getKey();

    V getValue();
}

当我们定义好了泛型接口或者泛型类以后,就可以愉快的派生子类了。

//第一种写法
public class IPhoneApple<T> extends Apple<T> {

}
//第2种写法,Apple 中的 T 类型会被当做Object类型
public class IPhoneApple extends Apple {

}
//第三种写法
public class IPhoneApple extends Apple<String> {

}

上面3种写法都可以,但是我们继承泛型类或者实现泛型接口的目的何在?是为了得到一个类型明确的具体的类,所以应该选用第三种写法。下面定义一个具体的类继承Apple类并实现Pair接口。

public class IPhoneApple extends Apple<String> implements Pair<Integer, String> {

    private String iPhoneInfo;
    private int key;

    public IPhoneApple(String info, String iPhoneInfo, int key) {
        super(info);
        this.iPhoneInfo = iPhoneInfo;
        this.key = key;
    }

    //覆盖Apple中的方法
    @Override
    public String getInfo() {
        return super.getInfo();
    }

    //实现接口中的方法
    @Override
    public Integer getKey() {
        return key;
    }

    //实现接口中的方法
    @Override
    public String getValue() {
        return iPhoneInfo;
    }

    public static void main(String args[]) {
        IPhoneApple iPhoneApple = new IPhoneApple("手机信息:", "苹果几啊?", 7);
        //输出结果 手机信息:苹果几啊?7
        System.out.println(iPhoneApple.getInfo() + iPhoneApple.getValue() + iPhoneApple.getKey());
    }
}

类型通配符

问题引出:假设现在要定义一个方法,方法需要一个List类型的参数,但是List 里面元素类型是不确定的,那这个方法改怎样定义呢?

首先想到的是Object搓起来,不管你是什么形,反正你是Object。

public static void test(List<Object> objectList) {
    for (Object o : objectList) {
        System.out.println(o);
    }
}

然后调用一下这个方法。

public static void main(String args[]){
    List<String> stringList=new ArrayList<>();
    test(stringList);//编译不通过
}

但是意外出现了,竟然编译不通过。编辑器给的提示是:

test(java.lang.util.List<Object>) can not be applied to (java.lang.util.List<String>)

表明List<String>并不能当做List<Object>使用,说明List<String>并不是List<Object>的子类。

注意:如果Foo是Bar的一个子类(子类或者子接口)而G是具有泛型声明的类或者接口G<Foo>并不是G<Bar>的子类型!

可以使用通配符解决上面编译不通过的问题。把test方法的参数列表改为List<?> objectList 将一个?作为List 的类型形参,表示这个List中的元素可以匹配任何类型,无论是List<String>,还是List<Integer>都来者不拒。如下所示。

public static  void test(List<?> objectList) {
    for (Object o : objectList) {
        System.out.println(o);
    }
}

调用没问题。

List<String> stringList = new ArrayList<>();
test(stringList);
List<Integer> integerList = new ArrayList<>();
test(integerList);

但是新的问题来了。List<?> objectList 这样的List是不能愉快的向里面添加元素的。

//添加啥都不好使。
objectList.add(new Object());//编译不通过

List的 add 方法如下所示,往里面添加的元素的类型是E类型的。E 就是我们声明一个具体的List传入的实际类型参数 比如List<Strinng>,这个E 就是String。

boolean add(E e);
List<?> objectList

但是这样写的话,就无法确定要添加的元素是什么类型,所以就没法添加一个新的元素到List里面。

设定类型通配符的上限

前面我们的引出问题现在变了:假设现在要定义一个方法,方法需要一个List类型的参数但是List 里面元素类型是不确定的,但是有一点是可以肯定的,就是这些元素具有共同的父类,我们可以放三角形,圆形,长方形等等,它们有共同的父类Shape。

Shape 类。

public abstract class Shape {

    public abstract void draw();

}

我们的圆形继承自Shape。

public class Circle extends Shape {

    @Override
    public void draw() {
        System.out.println("画一个圆形");
    }
}

我们的长方形继承自Shape。

public class Rectangle extends Shape {

    @Override
    public void draw() {
        System.out.println("画一个长方形");
    }
}

现在test方法就可以这样定义了。

 public static void test(List<? extends Shape> shapeList) {
    for (Shape shape : shapeList) {
        shape.draw();
    }
}

test 方法的形参 List<? extends Shape> 中的 ? 代表 List 中的元素类型是Shape类型或者是Shape的子类。但是到底是 Circle ,还是 Rectangle 还是Shape 是不确定的。

调用test方法。

//加入圆形
List<Circle> shapeCircles = new ArrayList<>();
shapeCircles.add(new Circle());
test(shapeCircles);
//加入长方形
List<Rectangle> shapeRectangle = new ArrayList<>();
shapeRectangle.add(new Rectangle());
test(shapeRectangle);

输出结果,也是可以的。

画一个圆形
画一个长方形

但是由于? 表示Shape 的未知类型,向List<? extends Shape> shapeList中添加元素也是不行的。

shapeList.add(new Rectangle());//编译不通过

Java 泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时,设定上限,用于限定传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。如下所示:

public class MyApple<T extends Number> {

    T number;

    public static void main(String[] args) {
        MyApple<Integer> ai = new MyApple();
        MyApple<Double> ad = new MyApple();
        //下面代码编译异常,因为String 不是 Number 的子类。
        MyApple<String> as=new Apple<>();//注释1
    }
}

上面定义了MyApple泛型类,MyApple类的类型形参上限是Number类,这表明使用MyApple的时候为形参T传入的实际类型参数只能是Number或者Number的子类。上面代码注释1的地方传入的实际类型是String类型,既不是Number类型,又不是Number的子类,所以编译会出错。

**泛型方法:**就是在声明方法时定义一个或多个类型形参。格式如下。方法声明中声明得类型形参只能在方法内部使用。

修饰符<T,S,...>返回值类型 方法名(形参类表){
	//方法体...
}

上面的尖括号里面表示一个或者多个类型参数,用逗号隔开。下面定义一个具体的泛型方法,用于把一个集合中的元素,拷贝到一个Collection里面。

public static <T> void fromArrayToCollection(Collection<T> a, Collection<T> c) {
    for (T t : a) {
        c.add(t);
    }
}

调用妥妥的。

//String 类型
Collection<String> stringa = new ArrayList<>();
Collection<String> stringCollection = new ArrayList<>();
fromArrayToCollection(stringa, stringCollection);

我们发现我们在调用泛型方法的时候并没有传入实际的类型参数。是因为编译器根据我们传递的实际的参数类型,推断出了类型形参的实际类型。
但是你要是调用方法的时候传递的实际参数类型有问题,那编译器就懵了。如下所示

Collection<Integer> inta1= new ArrayList<>();
Collection<String> stringCollection1 = new ArrayList<>();
//调用下面方法就有问题了。编译器无法确定类型形参T应该是Integer还是String.
fromArrayToCollection(inta1, stringCollection1);

为了避免这种错误,可以修改fromArrayToCollection方法如下所示:

public static <T> void fromArrayToCollection(Collection<? extends T> a, Collection<T> c) {
    for (T t : a) {
        c.add(t);
    }
}

设定类型通配符的下限

? super Type ,这个通配符表示它必须是Type本身,或者Type的父类,所以称之为下限。

public static <T> T copy(Collection<? super T> des, Collection<T> src) {
    T last = null;
    for (T t : src) {
        last = t;
        des.add(t);
    }
    return last;
}

通配符:List<?>,可以添加数据,取出来的数据只能用Object类型来引用。

通配符上限:List<? extends Shape> 可以从集合里面取数据,不能添加。为什么不能添加呢?因为集合里放的是Shape类或者Shape的子类。比如Shape的子类有Square和Circle,如果我既往里面放Square又放Circle,那么不就出问题了。

通配符下限:List<? super Shape> 可以往集合里面添加数据,不能取数据(取出来的元素只能用Object来引用)。为什么不能取呢?因为? super Type ,这个通配符表示它必须是Type本身,或者Type的父类,取出来的是什么东西呢?不知道,只能是Object。

参考:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值