【java筑基】吃透泛型(一万字长文,建议收藏)

简介: 【java筑基】吃透泛型(一万字长文,建议收藏)

一、泛型介绍

1 泛型入门

集合元素过去默认为Object类型,无法指定元素类型,编译时不检查类型,而且每次取出对象都要进行强制类型转换,泛型出现避免了这种臃肿的代码。下列代码会看到编译时不检查元素类型导致的异常。

    public class ListErr {
      public static void main(String[] args) {
        List strList = new ArrayList();
        strList.add("人有悲欢离合");
        strList.add("月有阴晴圆缺");
        strList.add(23333);
        //ClassCastException
        strList.forEach(str -> System.out.println(((String) str).length()));
      }
    }

泛型用于指定集合存储数据的类型。

List<String> books=new ArrayList<String>();

上述代码定义集合时使用泛型,创建对象时构造器也给出泛型类型,这样显然是多余的。java7做了改进。

List<String> books=new ArrayList<>();

2 深入泛型

2.1定义泛型接口、类

我们可以在定义一个类时允许它使用泛型,通过阅读java提供的集合接口源码可以知道如何定义泛型接口。

    //定义接口时指定了一个类型形参E
    public interface List<E> extends Collection<E> {
        //在接口中将形参E作为类型使用
           boolean add(E e);
           Iterator<E> itorator;
       // ...
    }

在实际使用时,只需要在声明对象时传入E的实参即可 。

2.2 从泛型类派生子类

从泛型类派生子类时,我们可以为泛型指定实参,也可以不使用,注意不要再使用形参T。

    public class Apple<T> {
      private T info;
      public T getInfo(){
        return this.info;
      }
    }
    public class SmallApple extends Apple<String> {
      private String info;
      public String getInfo() {
        return this.info;
      }
    }

2.3 并不存在泛型类

实际上,泛型只是设计来用于方便编程,并不会由于指定类型不同而生成不同的class文件。

    public class GenericTest {
      public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();
        // true
        System.out.println(strList.getClass() == intList.getClass());
      }
    }

也就是说,不管传入实参是不是一个类型,它们仍然被当成一个类型的数据,不允许在静态成员中使用泛型形参。

二、通配符

如果在使用时泛型类时不传入泛型实参会出现警告,但是如果我们并不能确定其类型如何处理?第一种想法是传入Object类型的实参,但是实际上这种办法是行不通的。

    public class Test {
      public void listTest(List<Object> c){
        for(int i=0;i<c.size();i++){
          System.out.println(c.get(i));
        }
      }
      public static void main(String[] args) {
        List<String> strList=new ArrayList<>();
        //编译不能通过,The method testList(List<String>) is undefined for the type Test
        //testList(strList);
      }
    }

这是因为Java中List并不是List的子类,指定类型为Object传入参数类型只能是Object。

为了解决这个需求,可以使用类型通配符。

    public void listTest(List<?> c){
      for(int i=0;i<c.size();i++){
        System.out.println(c.get(i));
      }
    }

List表明他是任何泛型List的父类,现在任何的List类型都可以调用listTest()方法。上面的代码解决了不指定类型抛出警告的问题,在有的时候却会使代码臃肿:使用了泛型还要进行强制类型转换。我们也可以设定通配符上限,只代表某一类泛型的父类。

    public abstract class shape {
      public abstract void draw(Canvas c);
    }
    public class Circle extends Shape {
        @Override
      public void draw(Canvas c) {
        System.out.println("在画布" + c + "上画个圈圈");
      }
    }
    public class Retangle extends Shape {
      @Override
      public void draw(Canvas c) {
        System.out.println("在画布" + c + "上画个块块");
      }
    }
    public class Canvas {
      public void drawAll(List<? extends Shape> shapes) {
        for (Shape s : shapes) {
          s.draw(this);
        }
      }
    }

在并不知道受限制的通配符的具体类型时,不可以将Shape及其子类对象加入这个泛型集合中。

    public void addRetangle(List<? extends Shape> shapes){
    //    shapes.add(0, new Retangle());
    }

三、泛型类与泛型方法

1.泛型类

不仅使用通配符时可以设置形参上限,定义类型形参时也可以设置类型上限。

    public class Apple<T extends Number> {
      private T info;
      public static void main(String[] args) {
        Apple<Integer> intApple=new Apple<>();
        //编译不通过,Bound mismatch
        //Apple<String> strApple=new Apple<>();
      }
    }

而且Java可以在定义形参时设置一个父类上限,多个接口上限。下列代码要求T类型必须是Number类的子类,而且必须实现了java.io.Seriazable接口。

    public class Apple<T extends Number & java.io.Serializable> {...}

2.泛型方法

可以单独为方法指定泛型形参。在该方法内部可以把指定的泛型形参当成正常类型使用。

    public class GenericMethodTest {
      static <T> void fromArrayToCollections(T[] a,Collection<T> c){
        for(T o: a){
          c.add(o);
        }
      }
  public static void main(String[] args) {
    Object [] oa=new Object[100];
    Collection<Object> co=new ArrayList<>();
    //下列T代表Object类型
    fromArrayToCollections(oa, co);
    String [] sa=new String[100];
    Collection<String> cs=new ArrayList<>();
    //下列T代表String类型
    fromArrayToCollections(sa, cs);
    //下列T代表Object类型
    fromArrayToCollections(sa, co);
    Number [] na=new Number[100];
    //The method fromArrayToCollections(T[], Collection<T>) 
    //in the type GenericMethodTest is not applicable for 
    //the arguments (Number[], Collection<String>)
    //fromArrayToCollections(na, cs);
  }
 }

四、泛型方法的自动类型推断

编译器会根据泛型方法传入的实参自动推断形参的值,通常会推断出最直接的类型参数。


为了让编译器可以推断出泛型类型,不要让编译器迷惑,否则就会出错。

public class ErrorTest {
    static <T> void test(Collection<T> from,Collection<T> to) {
        for (T ele : from){
            to.add(ele);
        }
    }
    public static void main(String[] args) {
        List<String> as = new ArrayList<>();
        List<Object> ao = new ArrayList<>();
        //The method test(Collection<T>, Collection<T>) 
        //in the type ErrorTest is not applicable for 
        //the arguments (List<Object>, List<String>)
        //test(as, ao);
    }
}


可以进行如下修改,避免编译失败。

public class ErrorTest {
    static <T> void test(Collection<? extends T> from,Collection<T> to) {
        for (T ele : from){
            to.add(ele);
        }
    }
    public static void main(String[] args) {
        List<String> ao = new ArrayList<>();
        List<Object> as = new ArrayList<>();
        test(ao, as);
    }
}

五、泛型通配符与泛型方法区别

什么时候使用泛型方法,什么时候使用类型通配符呢?一般能够使用通配符,都可以改写为泛型方法。

    public interface Collection<E> extends Iterable<E> {
      boolean containsAll(Collection<?> c);
      boolean addAll(Collection<? extends E> c);
    }
    public interface Collection<E> extends Iterable<E> {
      <T> boolean containsAll(Collection<T> c);
      <T extends E> boolean addAll(Collection<T> c);
    }

这两个方法的类型形参T其实都只使用了一次,唯一效果就是在调用时传入实际类型参数,因此Collection接口设计时采用的时上示第一种:类型通配符,类型通配符就是被设计来支持灵活子类化的。

泛型方法用来表示方法一个或者多个参数之间的依赖关系,或者参数与返回值的关系,如果没有这种依赖关系,就不要使用泛型方法。

有时候会同时使用泛型通配符和泛型方法。比如Collctions.copy()方法。dest和src的参数之间存在依赖关系:src的集合元素必须是dest类型或其子类,故用泛型方法表示dest的类型,而该方法无需向src中添加修改元素,也没有其它参数src的类型,因此使用通配符更合适。

     public static <T> void copy(List<? super T> dest, List<? extends T> src) {...}

六、泛型构造器、设置通配符下限

1.泛型构造器

java中也支持泛型构造器。在泛型类中允许使用菱形语法,但不允许在显示声明构造器泛型类型的情况下使用菱形语法。

    class Foo <E>{
      public <T> Foo(T t) {
        System.out.println(t);
      }
    }
    public class GeneratorConstructor {
      public static void main(String[] args) {
        new Foo("人不轻狂枉少年");
        new <Integer> Foo(20);
        //使用菱形语法,E传入实参为String类型,E泛型构造器中T参数传入实参为Integer
        Foo <String> foo=new Foo<>(5);
        //E传入实参为String类型,显示声明构造器中的方法T传入实参为Integer
        Foo <String> foo2=new <Integer> Foo <String>(5); 
        //编译错误
        //Explicit type arguments cannot be used with '<>' in an allocation expression
        //Foo <String> foo3=new <Integer> Foo <>(5);
      }
    }

2.设置通配符下限

考虑场景:在copy方法中吧集合src中的元素复制到dest集合中,同时要求返回最后一个添加的元素。如果我们采用设置通配符上限的方法,那么返回最后一个添加的元素时,将返回一个丢失实际类型的参数。

    public class MyUtils_1 {
      public static <T> T copy(Collection<T> dest,
          Collection<? extends T> src){
        T last = null;
        for(T ele : src){
          last = ele;
          dest.add(ele);
        }
        //返回的类型为T,但last元素实际上可能是T的子类
        return last;
      }
    }

Java中设计了类型通配符下限解决这一需求。

    public class MyUtils {
      //下面dest集合元素的类型必须与src元素的类型相同,或是其父类
      public static <T> T copy(Collection<? super T> dest,
          Collection<T> src){
        T last = null;
        for(T ele : src){
          last = ele;
          dest.add(ele);
        }
        return last;
      }
    }

七、java8改进的泛型参数推断机制

java8增强了泛型方法的类型推断能力:允许通过调用方法的上下文推断类型参数的目标类型,允许在方法调用链中将推断到的泛型参数传递至最后一个方法。

    class MyUtil<E>{
      public static <Z> MyUtil<Z> nil(){
        return null;
      }
      public static <Z> MyUtil<Z> cons(Z head,MyUtil<Z> tail){
        return null;
      }
      E head(){
        return null;
      }
    } 
    public class InferenceTest {
      public static void main(String[] args) {
        //通过方法赋值的目标参数来推断类型参数为String
        MyUtil<String> ls=MyUtil.nil();
        //通过cons()方法所以需要的参数类型来推断类型参数的类型为Integer
        MyUtil.cons(42, MyUtil.nil());
      }
    }

八 泛型擦除与转换

当把一个带有泛型信息的变量赋值给一个不带泛型信息的变量时,泛型信息将被擦除,对元素的类型参数检查将变成类型的上限。

    class Apple<T extends Number>{
      T size;
      public Apple(){
      }
      public T getSize() {
        return size;
      }
      public void setSize(T size) {
        this.size = size;
      }
    }
    public class ErasureTest {
      public static void main(String[] args) {
        Apple<Integer> ap=new Apple<>();
        Integer apSize=ap.getSize();
        Apple ap2=ap;
        //Type mismatch: cannot convert from Number to Integer
        //Integer apSize2=ap2.getSize();
      }
    }

理论上,List是List的子类,把List对象直接赋给List应该出现编译错误,但实际上不会。

    public class ErasureTest2 {
      public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(1);
        // 泛型擦除
        List list = li;
        System.out.println(li.get(0));
        // 警告:The expression of type List needs unchecked conversion to conform
        // to
        List<String> ls = list;
        // Exception :java.lang.Integer cannot be cast to java.lang.String
        System.out.println(ls.get(0));
      }
    }

这篇文章就介绍到这里了。

相关文章
|
4月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
100 2
|
2月前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
53 0
[Java]泛型
|
2月前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
22 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()` 方法或自动装箱/拆箱机制创建。
47 9
Java——包装类和泛型
|
2月前
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
29 5
|
2月前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
21 1
|
2月前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
23 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、异常处理机制、反射
|
3月前
|
存储 安全 搜索推荐
Java中的泛型
【9月更文挑战第15天】在 Java 中,泛型是一种编译时类型检查机制,通过使用类型参数提升代码的安全性和重用性。其主要作用包括类型安全,避免运行时类型转换错误,以及代码重用,允许编写通用逻辑。泛型通过尖括号 `&lt;&gt;` 定义类型参数,并支持上界和下界限定,以及无界和有界通配符。使用泛型需注意类型擦除、无法创建泛型数组及基本数据类型的限制。泛型显著提高了代码的安全性和灵活性。
|
2月前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
24 0