你了解泛型吗?

简介: 面向对象编程中,多态算是一种泛化机制。你可以将方法的参数类型设置为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数,这样的方法将会更具有通用性。此外,如果将方法参数声明为接口,将会更加灵活。

写在前面

面向对象编程中,多态算是一种泛化机制。你可以将方法的参数类型设置为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数,这样的方法将会更具有通用性。此外,如果将方法参数声明为接口,将会更加灵活。

在 Java 增加泛型类型之前,通用程序的设计就是利用继承实现的,例如:ArrayList 类只维护一个 Object 引用的数组,Object 为所有类基类。

public class BeforeGeneric {
    /**
     * 泛型之前的通用程序设计
     */
    static class ArrayList{
        private Object[] elements=new Object[0];
        public Object get(int i){
            return elements[i];
        }
        /**
         * 这里的实现,只是为了演示,不具有任何参考价值
         * */
        public void add(Object o){
            int length = elements.length;
            Object[] newElements = new Object[length+1];
            for(int i=0; i<length; i++){
                newElements[i] = elements[i];
            }
            newElements[length] = o;
            elements = newElements;
        }
    }
    public static void main(String[] args) {
        ArrayList stringValues = new ArrayList();
        //可以向数组中添加任何类型的对象
        stringValues.add(1);
        //问题1——获取值时必须强制转换
        String str = (String) stringValues.get(0);
        //问题2——上述强制转型编译时不会出错,而运行时报异常java.lang.ClassCastException
        System.out.println(str);
    }
}
复制代码

面临的问题

1、当我们获取一个值的时候,必须进行强制类型转换。

2、当我们插入一个值的时候,无法约束预期的类型。假定我们预想的是利用 stringValues 来存放 String 集合,因为 ArrayList 只是维护一个 Object 引用的数组,我们无法阻止将 Integer 类型(Object 子类)的数据加入 stringValues。然而,当我们使用数据的时候,需要将获取的 Object 对象转换为我们期望的类型(String),如果向集合中添加了非预期的类型(如 Integer),编译时我们不会收到任何的错误提示。但当我们运行程序时却会报异常:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

at generic.BeforeGeneric.main(BeforeGeneric.java:24)

这显然不是我们所期望的,如果程序有潜在的错误,我们更期望在编译时被告知错误,而不是在运行时报异常。

什么是泛型

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

注意:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为 Object 类型。

使用泛型的好处

1、避免了类型强转的麻烦。

2、它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现 ClassCastException。

泛型的使用

泛型可以用在类、接口和方法上。

泛型方法

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法的规则:

  1. 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 )。
  2. 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  3. 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  4. 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。

定义格式:

修饰符 <代表泛型的变量> 返回值类型 方法名(参数){}
复制代码

示例:

public class GenericMethodTest {
    // 泛型方法 printArray                         
    public static < E > void printArray( E[] inputArray ) {
      // 输出数组元素            
         for ( E element : inputArray ){        
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }
    public static void main( String args[] ) {
        // 创建不同类型数组: Integer, Double 和 Character
        Integer[] intArray = { 1, 2, 3, 4, 5 };
        Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
        Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
        System.out.println( "整型数组元素为:" );
        printArray( intArray  ); // 传递一个整型数组
        System.out.println( "\n双精度型数组元素为:" );
        printArray( doubleArray ); // 传递一个双精度型数组
        System.out.println( "\n字符型数组元素为:" );
        printArray( charArray ); // 传递一个字符型数组
    } 
}
复制代码

输出如下所示:

整型数组元素为:
1 2 3 4 5 
双精度型数组元素为:
1.1 2.2 3.3 4.4 
字符型数组元素为:
H E L L O 
复制代码

泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

定义格式:

public class 类名 <泛型类型> {
}
复制代码

示例:

public class Box<T> {
    private T t;
    public void add(T t) {
        this.t = t;
    }
    public T get() {
        return t;
    }
    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        Box<String> stringBox = new Box<String>();
        integerBox.add(new Integer(10));
        stringBox.add(new String("菜鸟教程"));
        System.out.printf("整型值为 :%d\n\n", integerBox.get());
        System.out.printf("字符串为 :%s\n", stringBox.get());
    }
}
复制代码

输出如下所示:

整型值为 :10
字符串为 :菜鸟教程
复制代码

泛型 K、T、V、E、N、? 的含义

如果你点开 JDK 中一些泛型类的源码,我们会看到下面这些代码:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
...
}
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
...
}
复制代码

上面这些泛型类定义中的泛型参数 E、K 和 V 都是什么意思呢?其实这些参数名称是可以任意指定,就想方法的参数名一样可以任意指定,但是我们通常会起一个有意义的名称,让别人一看就知道是什么意思。泛型参数也一样,E 一般是指元素,用来集合类中。

常见泛型参数名称有如下:

T        Type 标识类型。

K,V    代表键值对中的 key 和 value。

E        代表 Element 通常在集合中使用。

N       代表 Number  数字。

?         无限通配符,表示的是未知类型,表示不关心或者不能确定实际操作的类型,一般配合容器类使用。

? 和 Object 的区别?


? 表示未知类型。Object 表示任意类型。 Object 在使用时需要强制转换为想要的类型。编译器只有在运行时才知道类型转换是否出现异常。

边界

当我们不指定或者不关心操作类型,但是又想进行一定范围限制的时候,我们可以通过添加上限或下限来起到限制作用。

上边界<? extends T>

  • 定义了上限,只有读的能力。此方式表示参数化的类型可能是所指定的类型,或者是此类型的子类。如果传入的类型不是 T 或者 T 的子类,编辑不成功。
  • 用于灵活读取,使得方法可以读取T 或 T 的任意子类型的容器对象。

示例:

public class Test {
    public static void printIntValue(List<? extends Number> list) {
        for (Number number : list) {
            System.out.print(number.intValue()+" "); 
        }
        System.out.println();
    }
    public static void main(String[] args) {
        List<Integer> integerList=new ArrayList<Integer>();
        integerList.add(2);
        integerList.add(2);
        printIntValue(integerList);
        List<Float> floatList=new ArrayList<Float>();
        floatList.add((float) 3.3);
        floatList.add((float) 0.3);
        printIntValue(floatList);
    }
}
复制代码

下边界<? super T>

  • 定义了下限,有读的能力以及部分写的能力,子类可以写入父类。此方式表示参数化的类型可能是指定的类型,或者是此类型的父类。如果传入的类型不是 T 或者 T 的父类,编辑不成功。
  • 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象。

示例:

public class Test {
    public static void fillNumberList(List<? super Number> list) {
        list.add(new Integer(0));
        list.add(new Float(1.0));
    }
    public static void main(String[] args) {
        List<? super Number> list=new ArrayList(); 
        list.add(new Integer(1));
        list.add(new Float(1.1));
    }
}
复制代码

注意

  • 要从泛型类取数据时,用 ? extends 通配符。
  • 要往泛型类写数据时,用 ? super 通配符。
  • 既要取又要写,就不用通配符(即 extends 与 super 都不用)。
  • 不能同时声明泛型通配符上界和下界。

泛型擦除

泛型值存在于编译期,代码在进入虚拟机后泛型就会被擦除掉,这个特性叫做类型擦除。

  1. 如果泛型没有设置类型上限,那么将泛型转化成Object类型。
  2. 如果设置了类型上限,那么将泛型转化成他的类型上限。
  3. 如果有多个类型上限。使用第一个作为原始类型(T extends Demo1 & Demo2)原始类型为Demo1。
相关文章
|
7月前
|
Java
|
7月前
|
安全 编译器 Scala
何时需要指定泛型:Scala编程指南
本文是Scala编程指南,介绍了何时需要指定泛型类型参数。泛型提供代码重用和类型安全性,但在编译器无法推断类型、需要提高代码清晰度、调用泛型方法或创建泛型集合时,应明确指定类型参数。通过示例展示了泛型在避免类型错误和增强编译时检查方面的作用,强调了理解泛型使用时机对编写高效Scala代码的重要性。
53 1
何时需要指定泛型:Scala编程指南
|
7月前
|
Java 编译器 语音技术
泛型的理解
泛型的理解
38 0
|
7月前
什么是泛型,泛型的具体使用?
什么是泛型,泛型的具体使用?
40 0
|
7月前
|
Java
什么是泛型, 泛型的具体使用
什么是泛型, 泛型的具体使用
38 0
|
7月前
|
存储 Java
什么是泛型, 泛型的具体使用?
什么是泛型, 泛型的具体使用?
|
存储 Java 编译器
对泛型的认识
对泛型的认识
|
Java 编译器 API
泛型-详解
泛型-详解
130 0
泛型-详解
|
安全 JavaScript Java
泛型中的 T、E、K、V、?等等,究竟是啥?
泛型中的 T、E、K、V、?等等,究竟是啥?
泛型中的 T、E、K、V、?等等,究竟是啥?
|
安全 Java 编译器
第10章 泛型
泛型是什么,以及泛型怎么用。
126 0