侧边栏壁纸
  • 累计撰写 10 篇文章
  • 累计创建 5 个标签
  • 累计收到 2 条评论
标签搜索

目 录CONTENT

文章目录

泛型

step1

有如下代码

double d1 = 123d;
Map map = new HashMap();
map.put("key",d1);
double d2 = (double) map.get("key");
System.out.println(d2);

以上代码会正常打印出 d2 的值

123.0

上述代码将 map.get(“key”) 的返回值强制转换为了一个 double 类型,如果不写强制类型转换,编译器会直接提示错误警告

double d2 = map.get("key");

image-20220426221417261

它告诉编写者,不能直接把一个 Object类型的值赋值给一个 double 类型,也就是说 map.get 方法的返回值是一个 Object 类型

分析一下

Map 是一个以 键值对 的形式存放数据的数据结构,它允许以任何类型的值作为键(key),任何类型的值作为值(value)。由于Object是所有类型的超类,所以 map.put(key,value)的两个形参都是 Object类型的

执行以下代码时

map.put("key",d1);

map 把 key 和 value 都向上转型成了 Object 保存起来,当通过 get(key) 去取到数据时,也返回的是转型之后的 Object 对象。因为 map 自己并不会记录值的原始类型到底是哪一种,无论哪一种肯定都是 Object,所以我们拿到 Object 对象之后需要进行一次对应的类型转换,成为我们需要的值

double d1 = 123d;
Map map = new HashMap();
map.put("key",d1);
double d2 = (double) map.get("key");
System.out.println(d2);
int i1 = (int)map.get("key");

以上代码在编辑器中不会有任何问题,但是如果执行,第6行就会出现异常

java.lang.Double cannot be cast to java.lang.Integer

只看第六行的代码完全没有任何问题

int i1 = (int)map.get("key");

将一个 Object 类型强转为一个 int 类型在 java 语法中是允许的,但是由于这个 key 中存放的实际值是一个 double 类型。所以实际运行时会变成 将一个 double 类型强转换为一个 int 类型,这在 java 中是不允许的,就会出现异常

double d1 = 123d;
Map map = new HashMap();
map.put("key",d1);
double d2 = (double) map.get("key");
System.out.println(d2);

上面的代码由于我们已经知道 key 对应的值类型为 double
所以当我们通过 map.get(“key”)拿到返回的 Object
已经知道该强转为哪一种类型,所以是完全没问题的

实际的工程代码中,想要查到这个 key 是在哪里、什么地方被 put 进 map 的,是不好查的,所以我们只能拿到 map.get 之后的 Object 对象。这个 Object 在被转换之前的原始类型是什么我们并不知道,但是由于语法上 Object 可以被转换成任何一个类型。所以编辑阶段编辑器是不会有任何提示的,只有运行时才能知道是否正确

Object o = map.get("key1");
// 可以将 o 转换成任意类型,编辑时不会有错误提示
int n = (int)o;
double d = (double)o;
YourClass y = (YourClass)o;
//... ...

能否在 map 定义之初就指定这个 map 中的 键和值 的类型呢?
调用 put、get 方法时的形参也按照指定的 键和值 的类型
这样在编码时就能知道 此map中的 键和值 应该是什么类型

step2

java1.5之后更新了泛型

现在定义一个 map 的方式

Map<Integer,String> map = new HashMap<>();

此种方式指定了这个 map 的键值只能是一个 Integer 类型,对应的值只能是一个 String 类型,这样在调用 map.put 方法时,可以直观的看到这个 map 的键值类型

image-20220426232837618

现在往 map 中存放两条键值对数据

map.put(1,"firstValue");
map.put(2,"secondValue");

然后 get 数据

String s = map.get(1);
System.out.println(s);
// firstValue

由于定义 map 之初就指定了 key 和 value 的类型,通过查看 map 的定义语句我们就能知道,返回类型是什么,就用返回的类型同类型的变量去接收即可,并且如果我们用一个其他类型的变量去接收,编辑器直接会提示代码错误

image-20220426233831737

解决了 step1中的问题

step3 定义泛型类型

自定义一个泛型

public class Gen <T>{
    private T one;
    private T two;

    public Gen(){}

    public Gen(T one,T two){
        this.one = one;
        this.two = two;
    }

    public T getOne() {
        return one;
    }

    public void setOne(T one) {
        this.one = one;
    }

    public T getTwo() {
        return two;
    }

    public void setTwo(T two) {
        this.two = two;
    }
}

上述代码含义是:
一个泛型类型 Gen,它的泛型 T 可以是任意一个类,属性 one、two 都是 T 类型的对象

我们可以指定它的泛型

public static void main(String[] args) {
  Gen<Number> numberGen = new Gen<Number>(123,456);
}

上面我们指定了 numberGen 的泛型为 Number,现在 numberGen 的两个属性 one、two 就都是 Number 类型,且 set 方法的形参也都为 Number 类型

再定义一个针对于 参数为 泛型是 Number 类型的 Gen 的方法

public static <T> int sum(Gen<Number> gen){
  Number one = gen.getOne();
  Number two = gen.getTwo();
  return one.intValue() + two.intValue();
}

这是一个简单的将 Gen 中的属性求和的方法,并且形参要求是一个 泛形为 Number 的 Gen 类型

 public static void main(String[] args) {
        Gen<Number> numberGen = new Gen<Number>(123,456);
        System.out.println(sum(numberGen));

        Gen<Integer> integerGen = new Gen<>(1,5);
        System.out.println(sum(integerGen));// 此处会有错误警告

    }

    public static <T> int sum(Gen<Number> gen){
        Number one = gen.getOne();
        Number two = gen.getTwo();
        return one.intValue() + two.intValue();
    }

按照 java 的语法逻辑,IntegerNumber 的子类,那么可以向上转型为 Number。而 sum 方法的形参中的 泛型 就是 Number,所以 integerGen 应该可以作为参数才对
为什么不可以这么做?

值得注意的是
虽然 Integer 是 Number 的子类可以向上转型
但是 sum 方法的形参是一个 泛型类型为 Number 的泛型类,并不是单纯的 Number
Gen<Integer> integerGen并不是 Gen<Number> numberGen的子类,所以不可作为形参

sum 方法的方法体对于一个 Number 或者 Number 的子类 同样适用

Number one = gen.getOne();
Number two = gen.getTwo();
return one.intValue() + two.intValue();

有没有一种方式能够让 泛型是 Number 或者 Number 的子类的泛型对象 Gen 也能作为形参传入呢?

step4 上界通配符

针对 step3 最后的问题,java 提供了一种方式可以做到

改写 sum 方法

public static <T> int sum(Gen<? extends Number> gen){
        Number one = gen.getOne();
        Number two = gen.getTwo();
        return one.intValue() + two.intValue();
   	}

Gen<? extends Number> gen的含义是

泛型类型形参 gen 的泛型可以是 Number 或者 Number 的子类。这种方式叫做 上界通配符,使用此种方式,所有 泛型为 Number 或者 Number 子类的 Gen 都可以作为参数传递进来,得到 intValue 的和

例如

Gen<Integer> integerGen = new Gen<>();
Gen<Double> doubleGen = new Gen<>();
Gen<Float> floatGen = new Gen<>();
// ... ...

现在 step3 中的代码就能正常执行了

 public static void main(String[] args) {
        Gen<Number> numberGen = new Gen<Number>(123,456);
        System.out.println(sum(numberGen));// 579

        Gen<Integer> integerGen = new Gen<>(1,5);
        System.out.println(sum(integerGen));// 6
    }

    public static <T> int sum(Gen<? extends Number> gen){
        Number one = gen.getOne();
        Number two = gen.getTwo();
        return one.intValue() + two.intValue();
    }

注:

使用上界通配符有一个需要注意的地方

先修改一下 sum 方法代码

public static <T> int sum(Gen<? extends Number> gen){
        Number one = gen.getOne();
        Number two = gen.getTwo();
        
        gen.setOne( new Integer(789) );// 此处会出现错误警告
        
        return one.intValue() + two.intValue();
    }

如果我们尝试在 sum 方法中修改 gen 的属性值,是不允许的,按照以往的经验,拿到了一个对象,通过 set 方法去修改对象的属性值是没有问题的
这里的原因是:

形参 gen 的泛型是 Number 或者它的子类,所以 set 方法的形参也是 Number 或者 其子类
但是 Number 理论上可以有无数个子类,在 sum 方法中并不能知道此时的 gen 泛型到底是 Number 的哪一个子类
所以 set 方法的形参到底是什么具体的类型在 sum 方法中无法知晓,而父类 Number 是无法转型为子类的
set 方法就无法被正常调用
只可以将其 set 为 null

gen.setOne(null);

除非特殊需要才会这样做,因为这样会导致后面取值调用时空指针异常

由上面的分析得知

使用上界通配符的方式可以读取 泛型类型中的泛型,但是无法修改 泛型类型中的泛型 – 只读

step5 下界通配符

上界通配符意义是 泛型可以是指定类型本身或者其子类

下界通配符的意义是 泛型可以是指定类型本身或者其父类

定义一个方法,参数使用 下界通配符 Gen<? super Integer>

public static <T> void set(Gen<? super Integer> gen,Integer one ,Integer two){
        gen.setOne(one);
        gen.setTwo(two);
}

这是一个简单的将 泛型为 Integer 的 gen 中属性重新赋值的方法

测试一下

public static void main(String[] args) {
        Gen<Number> numberGen = new Gen<Number>(123,456);
        System.out.println(numberGen.getOne().intValue());// 123
        // 重新赋值
        set(numberGen,100,100);
        System.out.println(numberGen.getOne().intValue());// 100

        Gen<Integer> integerGen = new Gen<Integer>(1,5);
        System.out.println(integerGen.getOne().intValue());// 1
    	// 重新赋值
        set(integerGen,3,4);
        System.out.println(integerGen.getOne().intValue());// 3

    }

public static <T> void set(Gen<? super Integer> gen,Integer one ,Integer two){
        gen.setOne(one);
        gen.setTwo(two);
}

可以看出 对于泛型是 Integer 或者其父类 Number,方法都可成功调用且修改属性 one 的值

注:

使用 下界通配符也有一个需要注意的地方

修改一下 set 方法

public static <T> void set(Gen<? super Integer> gen,Integer one ,Integer two){
        gen.setOne(one);
        gen.setTwo(two);

        Integer integer = gen.getOne();// 此处会出现错误警告
        
    }

当我们试图在 set 方法中取到 gen 的属性值时,会有错误警告

原因与 step4 中的上界通配符类似
形参 gen 的泛型是 Integer 或者其父类,get 方法中返回的值也为 Integer 及其父类,由于 set 方法中并不知道传递过来的 gen 实际的泛型是 Integer 还是其父类,虽然 Integer 的父类只能有一个我们可以找到,但是 Integer 的父类也可以有父类,父类也可以有父类 …,Gen<? super Integer> gen 其中的形参泛型可以是 Integer 及其父类、父类的父类 、父类的父类的父类 …,所以无法确定 gen.get方法的返回值类型到底是什么,这就导致了 get 方法无法被正常调用,想要拿到返回值只能使用 所有类型的父类 Object 去接收

Object o = gen.getOne();

但是这就导致了 step1 中相同的问题:无法知晓 Object 持有的到底是哪一种类型,如何做转换

由上面的分析得知

使用下界通配符的方式可以修改 泛型类型中的泛型,但是无法读取 泛型类型中的泛型 – 只写

step6 实践

如果现在需要一个方法:将一个Gen<Number>中的属性拷贝到另一个Gen<Number>中,就应该考虑到 step4step5中的可读性和可写性问题

public static <T> void copyT(Gen<? super T> t1 , Gen<? extends T> t2){
       t1.setOne( t2.getOne() );
       t1.setTwo( t2.getTwo() );
   }

要将 g2 中的属性拷贝到 g1
g2的数据是要被读取的,不需要去修改它的属性 === 只读g1的数据是要被修改的,不需要取到它的属性 === 只写

public static void main(String[] args) {
        Gen<Float> floatGen = new Gen<Float>(123f,345f);
        Gen<Float> floatGen2 = new Gen<Float>(678f,9f);
    	System.out.println(floatGen2.getOne().floatValue());// 678。0
        copyT(floatGen2,floatGen);
        System.out.println(floatGen2.getOne().floatValue()); // 123.0
}

public static <T> void copyT(Gen<? super T> t1 , Gen<? extends T> t2){
        t1.setOne( t2.getOne() );
        t1.setTwo( t2.getTwo() );
    }

成功复制

jdk源码中Collentions类中的 copy 方法形参使用的就是此种形式

image-20220428160908054

step7 总结

综上可知:java 泛型实际上是一种抽象,T 可以表示任意一个 除基本数据类型外的类
那么泛型类型就可以作为一个模板,持有除基本数据类型外的任意一个类

java 世界中基础的数据类型只有那么多种,满足不了复杂的需求
在这基础之上,设计了类作为一种程序员可以自己定义的数据类型,解决了很多问题
其他语言如 c,也提供了结构体这种方式
而后 java 基于泛型的思想,提供了多种容器去存放这些类
使得程序员可以灵活的存放和处理一系列数据
如最常使用的 List 系列 Map 系列

image

而这些容器又是如何设计,如何存放类的
使用数组、链表、还是二叉树,又是一个值得去研究的问题
知识都不是孤立的,它们紧密相连

1

评论区