Java的“泛型”特性,你以为自己会了?(万字长文)

简介: 使用Java的小伙伴,对于Java的一些高级特性一定再熟悉不过了,例如集合、反射、泛型、注解等等,这些可以说我们在平时开发中是经常使用到的,尤其是集合,基本是只要写代码没有用不到的,今天我们先来谈谈泛型。


使用Java的小伙伴,对于Java的一些高级特性一定再熟悉不过了,例如集合、反射、泛型、注解等等,这些可以说我们在平时开发中是经常使用到的,尤其是集合,基本是只要写代码没有用不到的,今天我们先来谈谈泛型


1. 定义



在了解一个事物之前,我们必定要先知道他的定义,所以我们就从定义开始,去一步一步揭开泛型的神秘面纱。


# 泛型(generics)


他是 JDK5 中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时检测到非法的类型数据结构。泛型的本质就是参数化类型,也就是所操作的数据类型被指定为一个参数


# 常见的泛型的类型表示


上面的 T 仅仅类似一个形参的作用,名字实际上是可以任意起的,但是我们写代码总该是要讲究可读性的。常见的参数通常有 :

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

T - Type(表示Java 类,包括基本的类和我们自定义的类)

K - Key(表示键,比如Map中的key)

V - Value(表示值)

? - (表示不确定的java类型)


但是泛型的参数只能是类类型,不能是基本的数据类型,他的类型一定是自Object的


注意:泛型不接受基本数据类型,换句话说,只有引用类型才能作为泛型方法的实际参数


2. 为什么要使用泛型?



说到为什么要使用,那肯定是找一大堆能说服自己的优点啊。


# 泛型的引入,是java语言的来讲是一个较大的功能增强。同时对于编译器也带来了一定的增强,为了支持泛型,java的类库都做相应的修改以支持泛型的特性。

(科普:实际上java泛型并不是 jdk5(2004发布了jdk5) 才提出来的,早在1999年的时候,泛型机制就是java最早的规范之一)


另外,泛型还具有以下的优点:


# 1.提交了java的类型安全

泛型在很大程度上来提高了java的程序安全。例如在没有泛型的情况下,很容易将字符串 123 转成 Integer 类型的 123 亦或者 Integer 转成 String,而这样的错误是在编译期无法检测。而使用泛型,则能很好的避免这样的情况发生。

# 2.不需要烦人的强制类型转换

泛型之所以能够消除强制类型转换,那是因为程序员在开发的时候就已经明确了自己使用的具体类型,这不但提高了代码的可读性,同样增加了代码的健壮性。

# 提高了代码的重用性

泛型的程序设计,意味着编写的代码可以被很多不同类型的对象所重用


在泛型规范正式发布之前,泛型的程序设计是通过继承来实现的,但是这样子有两个严重的问题:


① 取值的时候需要强制类型转换,否则拿到的都是 Object

② 编译期不会有错误检查

我们来看下这两个错误的产生


2.1 编译期不会有错误检查

public class DonCheckInCompile {
   public static void main(String[] args) {
       List list = new ArrayList();
       list.add("a");
       list.add(3);
       System.out.println(list);
  }
}

程序不但不会报错,还能正常输出

image.gif640.png


2.2 强制类型转换


public class DonCheckInCompile {
   public static void main(String[] args) {
       List list = new ArrayList();
       list.add("a");
       list.add(3);
       for (Object o : list) {
           System.out.println((String)o);
      }
  }
}

image.gif640.png


因为你并不知道实际集合中的元素到底是哪些类型的,所以在使用的时候也是不确定的,如果在强转的时候,那必然会带来意想不到的错误,这样潜在的问题就好像是定时炸弹,肯定是不允许发生的。所以这就更体现了泛型的重要性


3. 泛型方法



在 java 中,泛型方法可以使用在成员方法、构造方法和静态方法中。语法如下:


public <申明泛型的类型> 类型参数 fun();如 public <T> T fun(T t);这里的 T 表示一个泛型类型,<T> 表示我们定义了一个类型为 T 的类型,这样的 T 类型就可以直接使用了,且<T> 需要放在方法的返回值类型之前。T 即在申明的时候是不知道具体的类型的,只有的使用的时候才能明确其类型,T 不是一个类,但是可以当作是一种类型来使用。


下面来通过具体的例子来解释说明,以下代码将数组中的指定的两个下标位置的元素进行交换(不要去关注实际的需求是什么),第一种Integer 类型的数组

public class WildcardCharacter {
   public static void main(String[] args) {
       Integer[] arrInt = {1, 2, 3, 4, 5, 6, 7, 8, 9};
       change(arrInt, 0, 8);
       System.out.println("arr = " +Arrays.asList(arrInt));
  }
   /**
    * 将数组中的指定两个下标位置的元素交换
    *
    * @param arr         数组
    * @param firstIndex 第一个下标
    * @param secondIndex 第二个下标
    */
   private static void change(Integer[] arr, intfirstIndex, int secondIndex) {
       int tmp = arr[firstIndex];
       arr[firstIndex] = arr[secondIndex];
       arr[secondIndex] = tmp;
  }
}

640.png


第二种是 String 类型的数组

640.png


编译直接都不会通过,那是必然的,因为方法定义的参数就是Integer[] 结果你传一个 String[],玩呢。。。所以这个时候只能是再定义一个参数类型是  String[]的。


那要是再来一个 Double 呢?Boolean 呢?是不是这就产生问题了,虽然说这种问题不是致命的,多写一些重复的代码就能解决,但这势必导致代码的冗余和维护成本的增加。所以这个时候泛型的作用就体现了,我们将其改成泛型的方式。

/**
    * @param t           参数类型 T
    * @param firstIndex 第一个下标
    * @param secondIndex 第二个下标
    * @param <T>         表示定义了一个类型 为 T 的类型,否则没人知道 T 是什么,编译期也不知道
    */
   private static <T> void changeT(T[] t, intfirstIndex, int secondIndex) {
       T tmp = t[firstIndex];
       t[firstIndex] = t[secondIndex];
       t[secondIndex] = tmp;
  }
接下来调用就简单了
public static void main(String[] args) {
    //首先定义一个Integer类型的数组
       Integer[] arrInt = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    //将第 1 个和第 9 个位置的元素进行交换
       changeT(arrInt, 0, 8);
       System.out.println("arrInt = " +Arrays.asList(arrInt));
       // 然后在定义一个String类型的数组
       String[] arrStr = {"a", "b", "c", "d", "e", "f","g"};
  //将第 1 个和第 2 个位置的元素进行交换
       changeT(arrStr, 0, 1);
        System.out.println("arrStr = " +Arrays.asList(arrStr));
  }

image.gif640.png


问题迎刃而解,至于普通的泛型方法和静态的泛型方法是一样的使用,只不过是一个数据类一个属于类的实例的,在使用上区别不大(但是需要注意的是如果在泛型类中 静态泛型方法是不能使用类泛型中的泛型类型的,这个在下文的泛型类中会详细介绍的)。


最后在来看下构造方法

public class Father {
    public <T> Father(T t) {
    }
}

然后假设他有一个子类是这样子的

class Son extends Father {
    public <T> Son(T t) {
        super(t);
    }
}

这里强调一下,因为在 Father 类中是没有无参构造器的,取而代之的是一个有参的构造器,只不过这个构造方法是一个泛型的方法,那这样子的子类必然需要显示的指明构造器了。


  • 通过泛型方法获取集合中的元素测试

既然说泛型是在申明的时候类型不是重点,只要事情用的时候确定就可以下,那你看下面这个怎么解释?


image.gif640.png


此时想往集合中添加元素,却提示这样的错误,连编译都过不了。这是为什么?


因为此时集合 List<T> 的 add 方法,添加的类型为 T,但是很显然 T 是一个泛型,真正的类型是在使用时候才能确定的,但是 在 add 的并不能确定 T 的类型,所以根本就无法使用 add 方法,除非 list.add(null),但是这却没有任何意义


4. 泛型类



先来看一段这样的代码,里面的使用到了多个泛型的方法,无需关注方法到底做了什么

public class GenericClassTest{
    public static void main(String[] args) {
        //首先定义一个Integer类型的数组
        Integer[] arrInt = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        //将第 1 个和第 9 个位置的元素进行交换
       new GenericClassTest().changeT(arrInt, 0, 8);
        System.out.println("arrInt = " + Arrays.asList(arrInt));
        List<String> list = Arrays.asList("a", "b");
        testIter(list);
    }
    /**
     * @param t           参数类型 T
     * @param firstIndex  第一个下标
     * @param secondIndex 第二个下标
     * @param <T>         表示定义了一个类型 为 T 的类型,否则没人知道 T 是什么,编译期也不知道
     */
    private <T> void changeT(T[] t, int firstIndex, int secondIndex) {
        T tmp = t[firstIndex];
        t[firstIndex] = t[secondIndex];
        t[secondIndex] = tmp;
    }
    /**
     * 遍历集合
     *
     * @param list 集合
     * @param <T>  表示定义了一个类型 为 T 的类型,否则没人知道 T 是什么,编译期也不知道
     */
    private static <T> void testIter(List<T> list) {
        for (T t : list) {
            System.out.println("t = " + t);
        }
    }
}

可以看到里面的 <T> 是不是每个方法都需要去申明一次,那要是 100 个方法呢?那是不是要申明 100 次的,这样时候泛型类也就应用而生了。那泛型类的形式是什么样子的呢?请看代码

public class GenericClazz<T>{
    //这就是一个最基本的泛型类的样子
}

下面我们将刚刚的代码优化如下,但是这里不得不说一个很基础,但是却很少有人注意到的问题,请看下面的截图中的文字描述部分。

640.png

image.gif

# 为什么实例方法可以,而静态方法却报错?

1. 首先告诉你结论:静态方法不能使用类定义的泛型,而是应该单独定义泛型

2. 到这里估计很多小伙伴就瞬间明白了,因为静态方法是通过类直接调用的,而普通方法必须通过实例来调用,类在调用静态方法的时候,后面的泛型类还没有被创建,所以肯定不能这么去调用的


所以说这个泛型类中的静态方法直接这么写就可以啦

/**
     * 遍历集合
     *
     * @param list 集合
     */
    private static <K> void testIter(List<K> list) {
        for (K t : list) {
            System.out.println("t = " + t);
        }
    }
  • 多个泛型类型同时使用

我们知道 Map 是键值对形式存在,所以如果对 Map 的 Key 和 Value 都使用泛型类型该怎么办?一样的使用,一个静态方法就可以搞定了,请看下面的代码

public class GenericMap {
    private static <K, V> void mapIter(Map<K, V> map) {
        for (Map.Entry<K, V> kvEntry : map.entrySet()) {
            K key = kvEntry.getKey();
            V value = kvEntry.getValue();
            System.out.println(key + ":" + value);
        }
    }
    public static void main(String[] args) {
        Map<String, String> mapStr = new HashMap<>();
        mapStr.put("a", "aa");
        mapStr.put("b", "bb");
        mapStr.put("c", "cc");
        mapIter(mapStr);
        System.out.println("======");
        Map<Integer, String> mapInteger = new HashMap<>();
        mapInteger.put(1, "11");
        mapInteger.put(2, "22");
        mapInteger.put(3, "33");
        mapIter(mapInteger);
    }
}

640.png

到此,泛型的常规的方法和泛型类已经介绍为了。


5. 通配符



通配符 ? 即占位符的意思,也就是在使用期间是无法确定其类型的,只有在将来实际使用时再指明类型,它有三种形式


  • <?> 无限定的通配符。是让泛型能够接受未知类型的数据
  • < ? extends E>有上限的通配符。能接受指定类及其子类类型的数据,E就是该泛型的上边界
  • <? super E>有下限的通配符。能接受指定类及其父类类型的数据,E就是该泛型的下边界


5.1 通配符之 <?>


上面刚刚说到了使用一个类型来表示反省类型是必须要申明的,也即<T> ,那是不是不申明就不能使用泛型呢?当然不是,这小节介绍的<?> 就是为了解决这个问题的。


<?> 表示,但是话又说话来了,那既然可以不去指明具体类型,那 ?就不能表示一个具体的类型也就是说如果按照原来的方式这么去写,请看代码中的注释

640.png

image.gif

而又因为任何类型都是 Object 的子类,所以,这里可以使用 Object 来接收,对于 ? 的具体使用会在下面两小节介绍


另外,大家要搞明白泛型和通配符不是一回事


5.2 通配符之 <? extend E>


<? extend E> 表示有上限的通配符,能接受其类型和其子类的类型 E 指上边界,还是写个例子来说明

public class GenericExtend {
    public static void main(String[] args) {
        List<Father> listF = new ArrayList<>();
        List<Son> listS = new ArrayList<>();
        List<Daughter> listD = new ArrayList<>();
        testExtend(listF);
        testExtend(listS);
        testExtend(listD);
    }
    private static <T> void testExtend(List<? extends Father> list) {}
}
class Father {}
class Daughter extends Father{}
class Son extends Father {    
}

这个时候一切都还是很和平的,因为大家都遵守着预定,反正 List 中的泛型要么是 Father 类,要么是 Father 的子类。但是这个时候如果这样子来写(具体原因已经在截图中写明了)


image.gif640.png



5.3 通配符之 <?super E>


<?super E> 表示有下限的通配符。也就说能接受指定类型及其父类类型,E 即泛型类型的下边界,直接上来代码然后来解释

public class GenericSuper {
    public static void main(String[] args) {
        List<Son> listS = new Stack<>();
        List<Father> listF = new Stack<>();
        List<GrandFather> listG = new Stack<>();
        testSuper(listS);
        testSuper(listF);
        testSuper(listG);
    }
    private static void testSuper(List<? super Son> list){}
}
class Son extends Father{}
class Father extends GrandFather{}
class GrandFather{}

640.png640.png因为 List<? super Son> list 接受的类型只能是 Son 或者是 Son 的父类,而 Father 和 GrandFather 又都是 Son 的父类,所以以上程序是没有任何问题的,但是如果再来一个类是 Son 的子类(如果不是和 Son 有关联的类那更不行了),那结果会怎么样?看下图,相关重点已经在图中详细说明


image.gif640.png


好了,其实泛型说到这里基本就差不多了,我们平时开发能遇到的问题和不常遇见的问题本文都基本讲解到了。最后我们再来一起看看泛型的另一个特性:泛型擦除


6. 泛型擦除



先来看下泛型擦除的定义

# 泛型擦除

因为泛型的信息只存在于 java 的编译阶段,编译期编译完带有 java 泛型的程序后,其生成的 class 文件中与泛型相关的信息会被擦除掉,以此来保证程序运行的效率并不会受影响,也就说泛型类型在 jvm 中和普通类是一样的。


别急,知道你看完概念肯定还是不明白什么叫泛型擦除,举个例子


public class GenericWipe {
    public static void main(String[] args) {
        List<String> listStr = new ArrayList<>();
        List<Integer> listInt = new ArrayList<>();
        List<Double> listDou = new ArrayList<>();
        System.out.println(listStr.getClass());
        System.out.println(listInt.getClass());
        System.out.println(listDou.getClass());
    }
}

image.gif640.png


image.gif640.png


这也就是说 java 泛型在生成字节码以后是根本不存在泛型类型的,甚至是在编译期就会被抹去,说来说去好像并没有将泛型擦除说的很透彻,下面我们就以例子的方式来一步一步证明


  • 通过反射验证编译期泛型类型被擦除

image.gif

class Demo1 {
    public static void main(String[] args) throws Exception {
        List<Integer> list = new ArrayList<>();
        //到这里是没有任何问题的,正常的一个 集合类的添加元素
        list.add(1024);
        list.forEach(System.out::println);
        System.out.println("-------通过反射证明泛型类型编译期间被擦除-------");
        //反射看不明白的小伙伴不要急,如果想看反射的文章,请留言反射,我下期保证完成
        list.getClass().getMethod("add", Object.class).invoke(list, "9527");
        for (int i = 0; i < list.size(); i++) {
            System.out.println("value = " + list.get(i));
        }
    }
}

640.png

打印结果如下:

640.png

image.gif

但是直接同一个反射似乎并不能让小伙伴们买账,我们为了体验差异,继续写一个例子

class Demo1 {
    public static void main(String[] args) throws Exception {
        //List<E>  实际上就是一个泛型,所以我们就不去自己另外写泛型类来测试了
        List<Integer> list = new ArrayList<>();
        //到这里是没有任何问题的,正常的一个 集合类的添加元素
        list.add(1024);
        list.forEach(System.out::println);
        System.out.println("-------通过反射证明泛型类型编译期间被擦除-------");
        list.getClass().getMethod("add", Object.class).invoke(list, "9527");
        for (int i = 0; i < list.size(); i++) {
            System.out.println("value = " + list.get(i));
        }
        //普通的类
        FanShe fanShe = new FanShe();
        //先通过正常的方式为属性设置值
        fanShe.setStr(1111);
        System.out.println(fanShe.getStr());
        //然后通过同样的方式为属性设置值 不要忘记上面的List  是 List<E> 是泛型哦!不要连最基本的知识都忘记了
        fanShe.getClass().getMethod("setStr", Object.class).invoke(list, "2222");
        System.out.println(fanShe.getStr());
    }
}
//随便写一个类
class FanShe{
    private Integer str;
    public void setStr(Integer str) {
        this.str = str;
    }
    public Integer getStr() {
        return str;
    }
}

image.gif640.png


测试结果显而易见,不是泛型的类型是不能通过反射去修改类型赋值的。


  • 由于泛型擦除带来的自动类型转换


因为泛型的类型擦除问题,导致所有的泛型类型变量被编译后都会被替换为原始类型。既然都被替换为原始类型,那么为什么我们在获取的时候,为什么不需要强制类型转换?


image.gif640.png


下面这么些才是一个标准的带有泛型返回值的方法。

public class TypeConvert {
    public static void main(String[] args) {
        //调用方法的时候返回值就是我们实际传的泛型的类型
        MyClazz1 myClazz1 = testTypeConvert(MyClazz1.class);
        MyClazz2 myClazz2 = testTypeConvert(MyClazz2.class);
    }
    private static <T> T testTypeConvert(Class<T> tClass){
        //只需要将返回值类型转成实际的泛型类型 T 即可
        return (T) tClass;
    }
}
class MyClazz1{}
class MyClazz2{}
  • 由泛型引发的数组问题

名字怪吓人的,实际上说白了就是不能创建泛型数组

640.png

image.gif


看下面的代码

640.png

image.gif

为什么不能创建泛型类型的数组?


因为List<Integer>List<String> 被编译后在 JVM 中等同于List<Object> ,所有的类型信息在编译后都等同于List<Object>,也就是说编译器此时也是无法区分数组中的具体类型是 Integer类型还是 String


但是,使用通配符却是可以的,我上文还特意强调过一句话:泛型和通配符不是一回事。请看代码


image.gif640.png


那这又是为什么?? 表示未知的类型,他的操作不涉及任何的类型相关的东西,所以 JVM 是不会对其进行类型判断的,因此它能编译通过,但是这种方式只能读不能写,也即只能使用 get 方法,无法使用 add 方法。

为什么不能 add ?<?> 提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空,另外上面也已经解释过为什么不能 add 的,这里就当做一个补充。


好了,关于泛型知识,今天就聊到这里,感谢大家的支持!



—————END—————





相关文章
|
27天前
|
存储 安全 Java
Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
【10月更文挑战第17天】Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
54 2
|
28天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
38 3
|
28天前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
32 2
|
30天前
|
存储 算法 Java
Java Set因其“无重复”特性在集合框架中独树一帜
【10月更文挑战第14天】Java Set因其“无重复”特性在集合框架中独树一帜。本文深入解析Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定的数据结构(哈希表、红黑树)确保元素唯一性,并提供最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的`hashCode()`与`equals()`方法。
28 3
|
1月前
|
安全 Java API
Java 17新特性让你的代码起飞!
【10月更文挑战第4天】自Java 8发布以来,Java语言经历了多次重大更新,每一次都引入了令人兴奋的新特性,极大地提升了开发效率和代码质量。本文将带你从Java 8一路走到Java 17,探索那些能让你的代码起飞的关键特性。
76 1
|
10天前
|
分布式计算 Java API
Java 8引入了流处理和函数式编程两大新特性
Java 8引入了流处理和函数式编程两大新特性。流处理提供了一种声明式的数据处理方式,使代码更简洁易读;函数式编程通过Lambda表达式和函数式接口,简化了代码书写,提高了灵活性。此外,Java 8还引入了Optional类、新的日期时间API等,进一步增强了编程能力。这些新特性使开发者能够编写更高效、更清晰的代码。
24 4
|
25天前
|
存储 Java API
优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。
【10月更文挑战第19天】本文介绍了如何优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。内容包括Map的初始化、使用Stream API处理Map、利用merge方法、使用ComputeIfAbsent和ComputeIfPresent,以及Map的默认方法。这些技巧不仅提高了代码的可读性和维护性,还提升了开发效率。
47 3
|
23天前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
19 0
[Java]泛型
|
25天前
|
存储 安全 Java
Java Map新玩法:深入探讨HashMap和TreeMap的高级特性
【10月更文挑战第19天】Java Map新玩法:深入探讨HashMap和TreeMap的高级特性,包括初始容量与加载因子的优化、高效的遍历方法、线程安全性处理以及TreeMap的自然排序、自定义排序、范围查询等功能,助你提升代码性能与灵活性。
24 2
|
1月前
|
Java 开发者
在Java的集合世界里,Set以其独特的特性脱颖而出,它通过“哈希魔法”和“红黑树防御”两大绝技
【10月更文挑战第13天】在Java的集合世界里,Set以其独特的特性脱颖而出。它通过“哈希魔法”和“红黑树防御”两大绝技,有效抵御重复元素的侵扰,确保集合的纯洁性和有序性。无论是“人海战术”还是“偷梁换柱”,Set都能从容应对,成为开发者手中不可或缺的利器。
31 6