Java从入门到精通九(Java泛型)

简介: 一般具有一些比较规范的泛型类型标记符。E - Element (在集合中使用,因为集合中存放的是元素)T - Type(Java 类)K - Key(键)V - Value(值)N - Number(数值类型)? - 表示不确定的 java 类型这种标记符可以用在类,接口,方法中,我们可以称之为泛型类,泛型接口,泛型方法。使用泛型的好处

泛型说明


泛型是什么?我们在哪里会遇到?

比如在一些集合类里面,我们可以看到对于键值的参数化限制。作用就是指定了键值的类型。



当然也有未知类型的时候指定泛型,这种比较灵活,根据传入的具体参数决定具体参数类型。



一般具有一些比较规范的泛型类型标记符。


E - Element (在集合中使用,因为集合中存放的是元素)

T - Type(Java 类)

K - Key(键)

V - Value(值)

N - Number(数值类型)

? - 表示不确定的 java 类型


这种标记符可以用在类,接口,方法中,我们可以称之为泛型类,泛型接口,泛型方法。


使用泛型的好处


1:在代码编译时期对数据类型进行检查


package java_practice;
import java.util.ArrayList;
public class GenericDemo {
    public static void main(String args[])
    {
        ArrayList  list = new ArrayList();
        list.add("jgdaabc");
        list.add(123);
        for(int i =0;i<list.size();i++)
        {
            String item = (String)list.get(i);
            System.out.println(item);
        }
    }
}


这个程序没有使用泛型,也就是没有对参数类型进行限制,集合中可以添加任意类型,但是如果我们后面的需求是String类型的话,我们需要转换。这样转换虽然在编译上没有报错,但是运行的时候便会抛出异常。



Integer类型是无法转换为String类型的。


其实我们可以去简单修改


package java_practice;
import java.util.ArrayList;
public class GenericDemo {
    public static void main(String args[])
    {
        ArrayList  list = new ArrayList();
        list.add("jgdaabc");
        list.add(123);
        for(int i =0;i<list.size();i++)
        {
            String item = list.get(i).toString();
            System.out.println(item);
        }
    }
}



我们也可以这样


package java_practice;
import java.util.ArrayList;
public class GenericDemo {
    public static void main(String args[])
    {
        ArrayList  list = new ArrayList();
        list.add("jgdaabc");
        list.add(123);
        for(int i =0;i<list.size();i++)
        {
            String item = String.valueOf(list.get(i));
            System.out.println(item);
        }
    }
}


关于这三种方式的区别,本文不做探讨。只是给出了解决办法。


但是我要说明的就是没有泛型的情况下,如果我们错误进行存储的话,倏然类型不可以转换,但是编译通过了。这样就可能在运行的时候抛出异常,但是如果我们很好的使用泛型,这样可以在编译的时候就可以避免这种错误。


ArrayList<String>  list = new ArrayList<String>();



给集合添加String类型进行限制,所以泛型为String,这样定义的话说明了集合中存储的元素只能是String类型。所以如果存储其它类型的话,就会在编译的时候进行检查。


2:让程序更加灵活


但是其实并不是说,我们使用泛型的时候类型一定是固定的。

简单举个例子


package java_practice;
import java.util.ArrayList;
public class GenericDemo<T> {
    public GenericDemo(T t) {
        System.out.println(t);
    }
    public static void main(String args[])
    {
        GenericDemo genericDemo = new GenericDemo("hello");
        new GenericDemo(123);
    }
}



T具体的类型由参入的参数决定


3:消除强制转换

其实道理还是和第一点的一样




提前将泛型写明,可以对后续的类型需求更加清楚。在其它的方面也是这样。


泛型类

class 类名 <T>{
  private T value1;
  /*public 类名(T value1)
  {
  this.value = value;
    }*/
    public T setValue(T value1)
    {
  this.value = value; 
    }
     public T getValue()
     {
  return value;
     }
}


大致的基本可以这样去定义,当然只要符合规范,自己也是可以去设定一些方法等的。类在实例化的时候一定要传入具体的参数。


一个例子


package java_practice;
import java.util.ArrayList;
import java.util.HashMap;
public class GenericDemo<T> {
    private T name;
    private T age;
    public void setName(T name) {
        this.name = name;
    }
    public void setAge(T age) {
        this.age = age;
    }
    public T getName() {
        return name;
    }
    public T getAge() {
        return age;
    }
    public GenericDemo(T t) {
        System.out.print(t);
    }
    public void print_demo() {
        System.out.println("我的名字叫" + name + ",今年" + age + "岁");
    }
    public static void main(String args[]) {
        GenericDemo demo_1 = new GenericDemo<>("hello,");
        demo_1.setAge(19);
        demo_1.setName("兰舟千帆");
        demo_1.print_demo();
    }
}


使用泛型给我们带来了一定的便捷。属性的确定可以根据传入参数的类型进行确定。


泛型接口

定义一个接口



public interface 类名<T>{
  public default T 方法名()
  {
  .....
    }
    public T demo_1();
}


实现接口类


public class 类名<T> implements 接口类名<T>
{
 @Override
    public T demo() {
      .....
    }
    @Override
    public T demo_1() {
     ......
    }
}


default关键字使得接口中的方法有方法体。这是最近的一个新的特性。

被default修饰的方法不再是抽象方法,我们甚至可以不去实现。


用泛型修饰方法的话,其实是代替了方法的返回类型。如果用泛型修饰后,又用其它的类型指定后只是冲突的。


引入类型通配符

这个我是查看了许多文章,然后其中说明的一个比较好的,我摘录一下具体的内容。


不变,协变,逆变

网上查看了许多资料就找到一篇用这几个名词。说实话,这样去描述类型转换或者继承我还是第一次见


逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);

f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;**

f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;**

f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系


注:该段摘录自java泛型 通配符详解及实践


这样的数学公式表达可能对于一些读者不是很友好。

简单来说,逆变就是比如A是B的父类,但是A可以继承B的类型匹配。。这样其实看起来有点逆向关系,所以叫做逆变,当然一般情况下,是不支持逆变的。

协变就是如果A是B的父类,B可以继承A的属性,也可以认为就是类型关系。

不变就是无论AB是和关系,都不能进行类型匹配。


这里的A或者B可以理解为Number或者Integer.然后f就代表了对应关系,A,B满足转型的条件的话,如果对应的数组是支持这种关系的,也就成立。


但是泛型是不变的,那就说明即使你的类型参数的转换满足了这种关系,也是绝对转换不了的。(以不变应万变)比如这样是不可以的。



用通配符支持协变和逆变(开挂行为)

解决上面问题的办法就是采用上边界通配符



加这个上边界通配符的作用就是说明了list被限制为继承number的任意类型。加了这个之后编译通过了,但是又带来了新的问题,既然是任意类型了,那么就没法再添加数据了,也就是无法添加一个确定的类型。除了null这个特殊的。null是不确定的,再mysql里面也经常说到null都不等于null。

上边界通配符的特点就是上界<? extends T>不能往里存,只能往外取。

既然有上边界通配符,那当然相对的也有下边界通配符。我们可以通过上边界通配符的特点发现,其实也就是帮助了支持协变,那么下边界通配符就是支持逆变。



这里的list2的类型就是Integer或者其父类


// An highlighted block
var foo = 'bar';


我们来看一个具体的说明的实例


 

import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    //一个基本的前提:父类赋值给子类,而子类可以赋值给父类
    public class GenericTest {
        public static void main(String[] args) throws Exception {
            class Animal {}
            class Bird extends Animal {}
            class Dog extends Animal {}
            class Cat extends Animal {}
            List<Animal> tmpList = new ArrayList<>();
            tmpList.add(1);
            tmpList.add(2);
            tmpList.add(3);
            // extendsList 元素的类型是 Animal 或其子类。
            List<? extends Animal> extendsList = tmpList;
            // add,set 的参数包括泛型,要将 Bird 转换为 Animal 的子类,由于具体子类不清楚,这是错误的
            extendsList.add(new Bird('a'));
            extendsList.set(1, new Bird('b'));
            // remove 的参数是 Object,不是泛型,因此没问题
            extendsList.remove(new Bird('a'));
            extendsList.contains(new Bird('b'));
            // get 的返回值为泛型 Animal 的子类,可以转换为父类 Animal
            Animal extendsGet = extendsList.get(1);
            // superList 元素的类型是 Animal 或其父类。
            List<? super Animal> superList = tmpList;
            // add,set 的参数包括泛型,要将 Integer 转换为 Number 的父类,这是没问题的
            superList.add(new Bird('a'));
            superList.set(1, new Bird('b'));
            // remove, contains 的参数是 Object,不是泛型,因此没问题
            superList.remove(new Bird(1));
            superList.contains(1);
            // get 返回值为泛型 Animal 的父类,Animal 的父类不可以转换为 Animal
            Animal superGet = superList.get(1);
    }



但是我觉得两行代码就足够理解了。



可以看出采用上边界通配符修饰是不能够添加数据的。但是下边界可以。

什么时候使用向上转,和向下转?


in"类型:

“in”类型变量向代码提供数据。 如copy(src,dest) src参数提供要复制的数据,因此它是“in”类型变量的参数。


"out"类型:

“out”类型变量保存接收数据以供其他地方使用.如复制示例中,copy(src,dest),dest参数接收数据,因此它是“out”参数。


“in”,“out” 准则

“in” 类型使用 上边界通配符? extends.

“out” 类型使用 下边界通配符? super.

如果即需要 提供数据(in), 又需要接收数据(out), 就不要使用通配符.


泛型方法


public void setName(T name) {
        this.name = name;
    }


这个叫泛型方法吗?并不叫


public T getName() {
        return name;
    }


这个也不叫泛型方法

这样可以


public <T> void getAge(T t)
    {
  ...
    }
  public<T>void show(T t)
  {
  ...
}



如果再泛型类中声明了泛型方法,泛型方法使用的泛型类型T可以与类中的T不是同一种类型,也就是T不等于T。


泛型方法与可变参数

public <T> void printfMsg(T... args)
    {
        for(T t:args)
        {
            System.out.println(t);
        }
    }


调用赋值


demo_1.printfMsg(1,2,3,4,5,6,7,8,9);


输出



泛型上界下界


其实还是和通配符的道理一样。上界就是<? extends 指定类型>

来举一个例子。字迹琢磨出来一个无聊的栗子,来吃个栗子



public <T>void  show_insert(GenericDemo<? extends Number> t) {
        System.out.println(t);
    }


我是如何调用这个方法给这个t赋值呢?


GenericDemo<Integer> demo2 = new GenericDemo<>(2);
  demo_1.show_insert(demo2);


相应的下界也是一样的道理


public <T>void  show_insert(GenericDemo<? super Integer> t) {
        System.out.println(t);
    }


GenericDemo<Integer> demo2 = new GenericDemo<>(2);
  demo_1.show_insert(demo2);



super以后你这个传入必须是指定类型或者是其父类型。


泛型数组

一般类型的数组直接指定泛型去操作是行不通的。可以用集合类型的数组,但是这样用的话,会有套娃的风险



其实这样做不一定有实用的价值,只是sun其实给出了这部分的有关说明。目前,对集合采用这样的操作自己不是怎么去用。


但是总说来。泛型合理使用还是对代码的优化很有帮助的。


自己以后要是遇到这方面的事情会再说明。就先菜到这里吧!该文是自己的一些认识,如果有不足或者说的不对的地方,还请指正。


相关文章
|
17天前
|
开发框架 IDE Java
java制作游戏,如何使用libgdx,入门级别教学
本文是一篇入门级教程,介绍了如何使用libgdx游戏开发框架创建一个简单的游戏项目,包括访问libgdx官网、设置项目、下载项目生成工具,并在IDE中运行生成的项目。
34 1
java制作游戏,如何使用libgdx,入门级别教学
|
5天前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
6 1
|
8天前
|
安全 Java 测试技术
🌟Java零基础-反射:从入门到精通
【10月更文挑战第4天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
13 2
|
13天前
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
23 5
|
10天前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
14 1
|
13天前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
16 2
|
1月前
|
Java 编译器 容器
Java——包装类和泛型
包装类是Java中一种特殊类,用于将基本数据类型(如 `int`、`double`、`char` 等)封装成对象。这样做可以利用对象的特性和方法。Java 提供了八种基本数据类型的包装类:`Integer` (`int`)、`Double` (`double`)、`Byte` (`byte`)、`Short` (`short`)、`Long` (`long`)、`Float` (`float`)、`Character` (`char`) 和 `Boolean` (`boolean`)。包装类可以通过 `valueOf()` 方法或自动装箱/拆箱机制创建。
30 9
Java——包装类和泛型
|
1月前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
1月前
|
Java 程序员 UED
Java中的异常处理:从入门到精通
【9月更文挑战第23天】在Java编程的世界中,异常是程序执行过程中不可避免的事件,它们可能会中断正常的流程并导致程序崩溃。本文将通过浅显易懂的方式,引导你理解Java异常处理的基本概念和高级技巧,帮助你编写更健壮、更可靠的代码。我们将一起探索如何捕获和处理异常,以及如何使用自定义异常来增强程序的逻辑和用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供有价值的见解和实用的技巧。
35 4
|
10天前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
11 0