Java进阶之泛型

简介: 【7月更文挑战第10天】Java泛型,自Java 5引入,旨在提升类型安全和代码重用。通过泛型,如List<String>,可在编译时捕获类型错误,防止ClassCastException。泛型包括泛型类、接口和方法,允许定义参数化类型,如`class className<T>`,并用通配符<?>、extends或super限定边界。类型擦除确保运行时兼容性,但泛型仅做编译时检查。使用泛型能增强类型安全性,减少强制转换,提高性能。

Java进阶之泛型
引出泛型
上节我们看了Java中的异常处理,知道Java中有很多的异常类型。其中有一种运行时异常叫类型转换异常: ClassCastException 异常。
运行下列代码:
public class ClassCastExceptionExample {
public static void main(String[] args) {
Object obj = new Integer(10);

          try {
              String str = (String) obj;
          } catch (ClassCastException e) {
              System.out.println("发生 ClassCastException!");
              e.printStackTrace();
          }
      }
  }
  我们创建了一个Integer对象,并将其赋值给一个Object类型的变量obj。然后,我们尝试将obj强制转换为String类型,但是由于obj实际上是一个Integer对象,而不是String对象,这个转换操作会失败,并抛出ClassCastException。
  报错:
  发生 ClassCastException!
  java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
      at ClassCastExceptionExample.main(ClassCastExceptionExample.java:6)
  在Java中,对象的向下转型都是有安全隐患的,比如上述的Integer强制转换为String。此时就会出现类型转换异常:ClassCastException。
  那怎么办呢? 在Java5的时候引入了一个叫泛型的东西,就是为了解决这种问题。
  比如我们在使用集合的时候,首先就会定义其泛型<String>:
  // 使用泛型 List,指定只能存储 String 类型的元素
  List<String> stringList = new ArrayList<>();
  stringList.add("Hello");
  stringList.add("World");
  // 使用泛型后,编译器会保证类型的正确性
  for (String str : stringList) {
      System.out.println(str);
  }
  // 下面这行代码会导致编译错误,因为不能将Integer添加到String类型的List中,因为就和上面一样:Integer强制转换为String就报错
  // stringList.add(10);
  // 使用泛型,即使在需要类型转换时,也不会出现ClassCastException
  // 因为编译器已经保证了类型的正确性
  System.out.println(stringList.get(0));
  可以看到,上面我们在定义List的时候就声明了泛型是String,这样在下面使用的时候,我们在编译代码的时候就能看到插入的类型是否会报错,就可以避免了运行时发生类型转换异常。
  由此可见,使用泛型可以在编译时期捕捉到类型错误,而不是在运行时。根据越早出错代价越小原则,这有助于提高代码的可靠性和稳定性。
  什么是泛型?
  泛型,即“参数化类型”,允许在定义类、接口、方法时不指定具体的类型,而是使用一个占位符(例如T、E、K、V等)来表示,在使用时再指定具体的类型。
  上面也说了,泛型是Java5中引入的,但是前面版本中的类怎么办呢?为了不受影响,默认就将泛型设置为<Object>,这样就实现兼容了,所以在使用原始类的时候,不加泛型也是可以的,但是会有警告,比如List list = new ArrayList<>();就会警告提示。
  Java7的时候,泛型可以由List<String> stringList = new ArrayList<String>();简写为List<String> stringList = new ArrayList<>();

  泛型分类
  泛型有三种使用方式:
  泛型类:
  在定义类时使用类型参数,这样类就可以被用于各种数据类型。声明泛型类非常简单,只需要在类名后添加一对尖括号,里面写上类型参数即可。
  // T1, T2, ..., Tn 是类型参数,它们在实例化泛型类时被替换为具体的类型。
  class className<T1, T2, ..., Tn> {
      private T1 t1;
      private T2 t2;
      public void set(T t) {
          this.t1 = t;
      }
  }
  泛型接口
  public interface Generator<T> {
      T next();
  }
  实现泛型接口时,可以选择指定具体类型,也可以继续保持泛型。
  public class NumberGenerator implements Generator<Integer> {
      private int index = 0;
      public Integer next() {
          return index++;
      }
  }
  泛型方法
  泛型方法是在方法级别使用泛型,即方法可以在调用时确定泛型参数的具体类型。可以在泛型类使用,也可以再非泛型类中使用。
  泛型方法的类型参数放在修饰符(如 public、static 等)之后,返回类型之前。
  <类型参数T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T。这个类型参数T可以出现在这个泛型方法的任意位置,泛型的数量也可以为任意多个 
  语法:
  修饰符 <类型参数T> 返回类型 方法名(参数列表) {
      // 方法体
  }
  示例:
  public class Util {
      public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
          return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue());
      }
  }
  在这个示例中,compare方法是一个泛型方法,它有两个类型参数K和V,用于比较两个Pair对象的键和值是否相等。
  泛型方法可以是静态的,也可以是实例的。在静态泛型方法中,类型参数是在调用方法时确定的,与类实例无关。

  泛型的通配符和上下限
  占位符:
  T是Type的缩写,比如 定义class Animal<T>{},使用的时候: Animal<Dog> dogs = new Animal<>();
  K是Key,V是Value,比如Map<K,V>, 使用的时候: HashMap<String, Integer> map = new HashMap<>();
  E是Element,常用在集合中,表示集合中的元素类型。
  N是Number,表示数字类型。
  S是Subtype,表示子类型。
  U是Another type,表示另一个类型参数。
  这些占位符并没有强制性的规定,开发者可以根据自己的喜好或者上下文来选择合适的字母作为泛型占位符。
  无界通配符(Unbounded Wildcards):
  无界通配符简单地使用<?>表示未知类型,它表示对类型没有限制。这种通配符通常用于处理泛型类型时不需要具体类型信息的情况。
  示例:
  public static void printList(List<?> list) {
      for (Object item : list) {
          System.out.println(item);
      }
  }
  在这个示例中,printList方法可以接受任何类型的List,但是不能向列表中添加元素,因为编译器不知道列表应该接受哪种类型。
  在Java中,通配符可以指定上限和下限.
  上限通配符使用 <? extends T> 表示,它指定泛型类型的上界,这意味着这个泛型类型必须是T类型或T类型的子类型。
  public class Fruit {}
  public class Apple extends Fruit {}
  public class Orange extends Fruit {}
  public static void main(String[] args) {
      List<Apple> apples = new ArrayList<>();
      apples.add(new Apple());
      // 使用上限通配符,可以传入Fruit或Fruit的子类型
      printFruitList(apples);
      List<Fruit> fruits = new ArrayList<>();
      fruits.add(new Fruit());
      fruits.add(new Apple());
      fruits.add(new Orange());
      // 同样可以使用上限通配符
      printFruitList(fruits);
  }
  public static void printFruitList(List<? extends Fruit> fruits) {
      for (Fruit fruit : fruits) {
          System.out.println(fruit);
      }
  }
  printFruitList 方法接受一个 List,其中包含的类型是 Fruit 或 Fruit 的任意子类型。这种方法可以安全地读取列表中的元素,因为它们至少是 Fruit 类型,但是不能向列表中添加元素,因为编译器不知道列表具体是 Fruit 的哪个子类型。

  下限通配符使用 <? super T> 表示,它指定泛型类型的下界,这意味着这个泛型类型必须是T类型或T类型的父类型。
  public static void addFruits(List<? super Fruit> fruits) {
      fruits.add(new Fruit());
      fruits.add(new Apple());
      fruits.add(new Orange());
  }
  public static void main(String[] args) {
      List<Fruit> fruits = new ArrayList<>();
      addFruits(fruits);
      List<Object> objects = new ArrayList<>();
      addFruits(objects); // 可以传入Object类型的列表,因为Object是Fruit的父类型
  }
  addFruits方法接受一个List,其中包含的类型是Fruit或Fruit的任意父类型。这种方法可以安全地向列表中添加Fruit或Fruit的子类型的元素,但是不能保证从列表中读取到的元素的类型,因为它们可能是 Object 类型。
  泛型擦除概念
  泛型信息只存在于代码编译阶段有效,但是在java的运行期(已经生成字节码文件后)与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
  例子:
  ArrayList<Integer> l1 = new ArrayList(); 
  ArrayList<String> l2 = new ArrayList();  
  System.out.println(l1.getClass()==l2.getClass()); 
  运行代码,结果为True
  这是因为ArrayList<String>和ArrayList<Integer>在jvm中的Class都是List.class,二者在jvm中等同于List<Object>,也说明泛型只是在编译的时候给我们增加了一个检查而已。

  使用泛型的好处:
  类型安全:在编译时进行类型检查,确保只能使用指定的类型。
  代码重用:同一个泛型类可以用于不同的数据类型,避免为每种数据类型编写重复的代码。
  性能提升:由于不需要进行类型转换,因此可以减少运行时错误,提高性能。

  定义的时候类型并不确定,在使用的时候才知道具体的类型就可以考虑泛型。
  END
目录
相关文章
|
2月前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
41 0
[Java]泛型
|
2月前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
19 1
|
3月前
|
Java 编译器 容器
Java——包装类和泛型
包装类是Java中一种特殊类,用于将基本数据类型(如 `int`、`double`、`char` 等)封装成对象。这样做可以利用对象的特性和方法。Java 提供了八种基本数据类型的包装类:`Integer` (`int`)、`Double` (`double`)、`Byte` (`byte`)、`Short` (`short`)、`Long` (`long`)、`Float` (`float`)、`Character` (`char`) 和 `Boolean` (`boolean`)。包装类可以通过 `valueOf()` 方法或自动装箱/拆箱机制创建。
45 9
Java——包装类和泛型
|
2月前
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
27 5
|
2月前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
19 1
|
2月前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
22 2
|
3月前
|
安全 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版)
|
3月前
|
存储 安全 搜索推荐
Java中的泛型
【9月更文挑战第15天】在 Java 中,泛型是一种编译时类型检查机制,通过使用类型参数提升代码的安全性和重用性。其主要作用包括类型安全,避免运行时类型转换错误,以及代码重用,允许编写通用逻辑。泛型通过尖括号 `&lt;&gt;` 定义类型参数,并支持上界和下界限定,以及无界和有界通配符。使用泛型需注意类型擦除、无法创建泛型数组及基本数据类型的限制。泛型显著提高了代码的安全性和灵活性。
|
2月前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
18 0
|
2月前
|
Java
【Java】什么是泛型?什么是包装类
【Java】什么是泛型?什么是包装类
23 0
下一篇
DataWorks